diff --git a/.github/workflows/packages_publishing.yml b/.github/workflows/packages_publishing.yml index fe93346a45..80bc770a21 100644 --- a/.github/workflows/packages_publishing.yml +++ b/.github/workflows/packages_publishing.yml @@ -8,12 +8,16 @@ jobs: - uses: actions/setup-java@v1 with: java-version: 11 + - name: Setup LibCurl + run: sudo apt install -y libcurl4-openssl-dev - name: Rewrite version run: | branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" cat gradle.properties | sed -e "s/^library_version=\([0-9\.]*\)/library_version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp rm gradle.properties mv gradle.properties.tmp gradle.properties + - name: KotlinSymbolProcessing execution + run: ./gradlew ksp - name: Build run: ./gradlew build - name: Publish to Gitea diff --git a/CHANGELOG.md b/CHANGELOG.md index 10033cfe9d..7b6b2607cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # TelegramBotAPI changelog +## 7.0.2 + +_This update brings experimental support of `linuxX64` and `mingwX64` platforms_ + +* `Versions`: + * `Kotlin`: `1.8.10` -> `1.8.20` + * `MicroUtils`: `0.17.5` -> `0.17.8` + * `Ktor`: `2.2.4` -> `2.3.0` +* `Core`: + * New `RequestsExecutor` - `MultipleClientKtorRequestsExecutor` + * Old `KtorRequestsExecutor` has been renamed to `DefaultKtorRequestsExecutor` + * `KtorRequestsExecutor` now is `expect class` + * On `JS`, `JVM` and `MinGWX64` platforms it is `DefaultKtorRequestsExecutor` + * On `LinuxX64` platform it is `MultipleClientKtorRequestsExecutor` + ## 7.0.1 * `Core`: diff --git a/README.md b/README.md index 4050520095..cdfc5b81c8 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # TelegramBotAPI [![Maven Central](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi/badge.svg)](https://maven-badges.herokuapp.com/maven-central/dev.inmo/tgbotapi) [![Supported version](https://img.shields.io/badge/Telegram%20Bot%20API-6.6-blue)](https://core.telegram.org/bots/api-changelog#march-9-2023) -| Docs | [![KDocs](https://img.shields.io/static/v1?label=Dokka&message=KDocs&color=blue&logo=kotlin)](https://tgbotapi.inmo.dev/index.html) [![Mini tutorial](https://img.shields.io/static/v1?label=Bookstack&message=Tutorial&color=blue&logo=bookstack)](https://bookstack.inmo.dev/books/telegrambotapi/chapter/introduction-tutorial) | -|:---:|:---:| -| Useful repos | [![Create bot](https://img.shields.io/static/v1?label=Github&message=Template&color=blue&logo=github)](https://github.com/InsanusMokrassar/TelegramBotAPI-bot_template/generate) [![Examples](https://img.shields.io/static/v1?label=Github&message=Examples&color=blue&logo=github)](https://github.com/InsanusMokrassar/TelegramBotAPI-examples/) | -| Misc | [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) [![Small survey](https://img.shields.io/static/v1?label=Google&message=Survey&color=blue&logo=google-sheets)](https://docs.google.com/forms/d/e/1FAIpQLSctdJHT_aEniyYT0-IUAEfo1hsIlezX2owlkEAYX4KPl2V2_A/viewform?usp=sf_link) | +| Docs | [![KDocs](https://img.shields.io/static/v1?label=Dokka&message=KDocs&color=blue&logo=kotlin)](https://tgbotapi.inmo.dev/index.html) [![Mini tutorial](https://img.shields.io/static/v1?label=Bookstack&message=Tutorial&color=blue&logo=bookstack)](https://bookstack.inmo.dev/books/telegrambotapi/chapter/introduction-tutorial) | +|:----------------------:|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| +| Useful repos | [![Create bot](https://img.shields.io/static/v1?label=Github&message=Template&color=blue&logo=github)](https://github.com/InsanusMokrassar/TelegramBotAPI-bot_template/generate) [![Examples](https://img.shields.io/static/v1?label=Github&message=Examples&color=blue&logo=github)](https://github.com/InsanusMokrassar/TelegramBotAPI-examples/) | +| Misc | [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) [![Small survey](https://img.shields.io/static/v1?label=Google&message=Survey&color=blue&logo=google-sheets)](https://docs.google.com/forms/d/e/1FAIpQLSctdJHT_aEniyYT0-IUAEfo1hsIlezX2owlkEAYX4KPl2V2_A/viewform?usp=sf_link) | +| Platforms | ![JVM](https://img.shields.io/badge/JVM-red?style=plastic&logo=openjdk&logoColor=white) ![Js](https://img.shields.io/badge/JavaScript-323330?style=plastic&logo=javascript&logoColor=F7DF1E) | +| Experimental Platforms | ![Linux x64](https://img.shields.io/badge/LinuxX64-FCC624?style=plastic&logo=linux&logoColor=black) ![MinGW x64](https://img.shields.io/badge/MinGWX64-black?style=plastic&logo=windows&logoColor=green) | diff --git a/gradle.properties b/gradle.properties index 7397299ac5..92de310c3b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ kotlin.incremental=true kotlin.incremental.js=true library_group=dev.inmo -library_version=7.0.1 +library_version=7.0.2 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f5379f8392..57ca01ab0d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -kotlin = "1.8.10" +kotlin = "1.8.20" kotlin-serialization = "1.5.0" kotlin-coroutines = "1.6.4" @@ -8,12 +8,12 @@ javax-activation = "1.1.1" korlibs = "3.4.0" uuid = "0.7.0" -ktor = "2.2.4" +ktor = "2.3.0" -ksp = "1.8.10-1.0.9" -kotlin-poet = "1.12.0" +ksp = "1.8.20-1.0.11" +kotlin-poet = "1.13.0" -microutils = "0.17.5" +microutils = "0.17.8" github-release-plugin = "2.4.1" dokka = "1.8.10" @@ -31,6 +31,8 @@ kotlin-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" } +ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" } ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" } ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor" } diff --git a/mppProjectWithSerialization.gradle b/mppProjectWithSerialization.gradle index b5b1d0585b..3a858f7e9a 100644 --- a/mppProjectWithSerialization.gradle +++ b/mppProjectWithSerialization.gradle @@ -13,6 +13,8 @@ kotlin { browser() nodejs() } + linuxX64() + mingwX64() sourceSets { commonMain { diff --git a/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSM.kt b/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSM.kt index 28463fe89b..a6df8c5018 100644 --- a/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSM.kt +++ b/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSM.kt @@ -93,8 +93,9 @@ interface BehaviourContextWithFSM : BehaviourContext, StatesMachine>, statesManager: StatesManager, + fallbackHandler: BehaviourWithFSMStateHandlerHolder? = null, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler() - ) = DefaultBehaviourContextWithFSM(behaviourContext, statesManager, handlers, onStateHandlingErrorHandler) + ) = DefaultBehaviourContextWithFSM(behaviourContext, statesManager, handlers, fallbackHandler, onStateHandlingErrorHandler) } } @@ -129,11 +130,12 @@ class DefaultBehaviourContextWithFSM( private val behaviourContext: BehaviourContext, private val statesManager: StatesManager, private val handlers: List>, + private val fallbackHandler: BehaviourWithFSMStateHandlerHolder? = null, private val onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler() ) : BehaviourContext by behaviourContext, BehaviourContextWithFSM { private val updatesFlows = mutableMapOf>() private val additionalHandlers = mutableListOf>() - private var actualHandlersList = additionalHandlers + handlers + private var actualHandlersList = additionalHandlers + handlers + listOfNotNull(fallbackHandler) protected val statesJobs = mutableMapOf() protected val statesJobsMutex = Mutex() @@ -250,6 +252,7 @@ class DefaultBehaviourContextWithFSM( behaviourContext.copy(bot, scope, broadcastChannelsSize, onBufferOverflow, upstreamUpdatesFlow, triggersHolder), handlers, statesManager, + fallbackHandler, onStateHandlingErrorHandler ) @@ -265,6 +268,7 @@ class DefaultBehaviourContextWithFSM( behaviourContext.copy(bot, scope, broadcastChannelsSize, onBufferOverflow, upstreamUpdatesFlow, triggersHolder), handlers, statesManager, + fallbackHandler, onStateHandlingErrorHandler ) } diff --git a/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSMBuilder.kt b/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSMBuilder.kt index 03750af9b8..f861bd75b4 100644 --- a/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSMBuilder.kt +++ b/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContextWithFSMBuilder.kt @@ -31,6 +31,7 @@ suspend fun TelegramBot.buildBehaviourWithFSM( defaultExceptionsHandler: ExceptionHandler? = null, statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), presetHandlers: List> = listOf(), + fallbackHandler: BehaviourWithFSMStateHandlerHolder? = null, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), block: CustomBehaviourContextReceiver, Unit> ): DefaultBehaviourContextWithFSM = BehaviourContextWithFSM( @@ -41,6 +42,7 @@ suspend fun TelegramBot.buildBehaviourWithFSM( ), presetHandlers, statesManager, + fallbackHandler, onStateHandlingErrorHandler ).apply { block() } @@ -59,6 +61,7 @@ suspend fun TelegramBot.buildBehaviourWithFSMAndStartLongPolling( defaultExceptionsHandler: ExceptionHandler? = null, statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), presetHandlers: List> = listOf(), + fallbackHandler: BehaviourWithFSMStateHandlerHolder? = null, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), timeoutSeconds: Seconds = 30, autoDisableWebhooks: Boolean = true, @@ -71,6 +74,7 @@ suspend fun TelegramBot.buildBehaviourWithFSMAndStartLongPolling( defaultExceptionsHandler, statesManager, presetHandlers, + fallbackHandler, onStateHandlingErrorHandler, block ).run { @@ -104,6 +108,7 @@ suspend fun TelegramBot.buildBehaviourWithFSM( defaultExceptionsHandler: ExceptionHandler? = null, statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), presetHandlers: List> = listOf(), + fallbackHandler: BehaviourWithFSMStateHandlerHolder? = null, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), block: CustomBehaviourContextReceiver, Unit> ): DefaultBehaviourContextWithFSM = BehaviourContextWithFSM( @@ -114,6 +119,7 @@ suspend fun TelegramBot.buildBehaviourWithFSM( ), presetHandlers, statesManager, + fallbackHandler, onStateHandlingErrorHandler ).apply { block() } @@ -137,6 +143,7 @@ suspend fun TelegramBot.buildBehaviourWithFSMAndStartLongPolling( defaultExceptionsHandler: ExceptionHandler? = null, statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), presetHandlers: List> = listOf(), + fallbackHandler: BehaviourWithFSMStateHandlerHolder? = null, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), timeoutSeconds: Seconds = 30, autoDisableWebhooks: Boolean = true, @@ -150,6 +157,7 @@ suspend fun TelegramBot.buildBehaviourWithFSMAndStartLongPolling( defaultExceptionsHandler, statesManager, presetHandlers, + fallbackHandler, onStateHandlingErrorHandler, block ).run { diff --git a/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBotWithFSM.kt b/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBotWithFSM.kt index 85a5f0d402..713a547e52 100644 --- a/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBotWithFSM.kt +++ b/tgbotapi.behaviour_builder.fsm/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBotWithFSM.kt @@ -46,6 +46,7 @@ suspend fun telegramBotWithBehaviourAndFSM( defaultExceptionsHandler: ExceptionHandler? = null, statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), presetHandlers: List> = listOf(), + fallbackHandler: BehaviourWithFSMStateHandlerHolder? = null, testServer: Boolean = false, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), timeoutSeconds: Seconds = 30, @@ -65,6 +66,7 @@ suspend fun telegramBotWithBehaviourAndFSM( defaultExceptionsHandler, statesManager, presetHandlers, + fallbackHandler, onStateHandlingErrorHandler, timeoutSeconds, autoDisableWebhooks, @@ -97,6 +99,7 @@ suspend fun telegramBotWithBehaviourAndFSMAndStartLongPolling( defaultExceptionsHandler: ExceptionHandler? = null, statesManager: StatesManager = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()), presetHandlers: List> = listOf(), + fallbackHandler: BehaviourWithFSMStateHandlerHolder? = null, testServer: Boolean = false, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), timeoutSeconds: Seconds = 30, @@ -116,6 +119,7 @@ suspend fun telegramBotWithBehaviourAndFSMAndStartLongPolling( defaultExceptionsHandler, statesManager, presetHandlers, + fallbackHandler, onStateHandlingErrorHandler, timeoutSeconds, autoDisableWebhooks, diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContext.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContext.kt index fbbd954d1b..69f299add4 100644 --- a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContext.kt +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/BehaviourContext.kt @@ -5,6 +5,7 @@ package dev.inmo.tgbotapi.extensions.behaviour_builder import dev.inmo.micro_utils.coroutines.* import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.handlers_registrar.TriggersHolder +import dev.inmo.tgbotapi.types.UpdateIdentifier import dev.inmo.tgbotapi.types.update.abstracts.Update import dev.inmo.tgbotapi.updateshandlers.* import kotlinx.coroutines.* @@ -71,9 +72,17 @@ class DefaultBehaviourContext( private val additionalUpdatesSharedFlow = MutableSharedFlow(0, broadcastChannelsSize, onBufferOverflow) override val allUpdatesFlow: Flow = (additionalUpdatesSharedFlow.asSharedFlow()).let { if (upstreamUpdatesFlow != null) { - var lastHandledUpdate = -1L + val handledUpdates = mutableSetOf() (it + upstreamUpdatesFlow).filter { - (it.updateId > lastHandledUpdate).also { passed -> if (passed) { lastHandledUpdate = it.updateId } } + val passed = handledUpdates.add(it.updateId) + (passed).also { passed -> + val needToDropCount = handledUpdates.size - broadcastChannelsSize + if (needToDropCount > 0) { + handledUpdates.removeAll( + handledUpdates.take(needToDropCount).ifEmpty { return@also } + ) + } + } } } else { it diff --git a/tgbotapi.behaviour_builder/src/linuxX64Main/kotlin/ActualDefaultCoroutineScopeProvider.kt b/tgbotapi.behaviour_builder/src/linuxX64Main/kotlin/ActualDefaultCoroutineScopeProvider.kt new file mode 100644 index 0000000000..57f3662698 --- /dev/null +++ b/tgbotapi.behaviour_builder/src/linuxX64Main/kotlin/ActualDefaultCoroutineScopeProvider.kt @@ -0,0 +1,6 @@ +package dev.inmo.tgbotapi.extensions.behaviour_builder + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +actual var defaultCoroutineScopeProvider: () -> CoroutineScope = { CoroutineScope(Dispatchers.Default) } diff --git a/tgbotapi.behaviour_builder/src/mingwX64Main/kotlin/ActualDefaultCoroutineScopeProvider.kt b/tgbotapi.behaviour_builder/src/mingwX64Main/kotlin/ActualDefaultCoroutineScopeProvider.kt new file mode 100644 index 0000000000..57f3662698 --- /dev/null +++ b/tgbotapi.behaviour_builder/src/mingwX64Main/kotlin/ActualDefaultCoroutineScopeProvider.kt @@ -0,0 +1,6 @@ +package dev.inmo.tgbotapi.extensions.behaviour_builder + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers + +actual var defaultCoroutineScopeProvider: () -> CoroutineScope = { CoroutineScope(Dispatchers.Default) } diff --git a/tgbotapi.core/build.gradle b/tgbotapi.core/build.gradle index e8448b7c47..e6eb4c507c 100644 --- a/tgbotapi.core/build.gradle +++ b/tgbotapi.core/build.gradle @@ -48,11 +48,23 @@ kotlin { api libs.javax.activation } } + + linuxX64Main { + dependencies { + api libs.ktor.client.curl + } + } + + mingwX64Main { + dependencies { + api libs.ktor.client.winhttp + } + } } } dependencies { - add("kspJvm", project(":tgbotapi.ksp")) + add("kspCommonMainMetadata", project(":tgbotapi.ksp")) } ksp { diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt index 88990f0996..b0b1996613 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt @@ -1,134 +1,25 @@ package dev.inmo.tgbotapi.bot.ktor -import dev.inmo.micro_utils.coroutines.runCatchingSafely -import dev.inmo.micro_utils.coroutines.safely import dev.inmo.tgbotapi.bot.BaseRequestsExecutor -import dev.inmo.tgbotapi.bot.TelegramBot -import dev.inmo.tgbotapi.bot.exceptions.* -import dev.inmo.tgbotapi.bot.ktor.base.* import dev.inmo.tgbotapi.bot.settings.limiters.ExceptionsOnlyLimiter import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter -import dev.inmo.tgbotapi.requests.abstracts.Request -import dev.inmo.tgbotapi.types.Response -import dev.inmo.tgbotapi.utils.* -import io.ktor.client.HttpClient -import io.ktor.client.plugins.* -import io.ktor.client.statement.bodyAsText -import io.ktor.client.statement.readText +import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper +import dev.inmo.tgbotapi.utils.nonstrictJsonFormat +import io.ktor.client.* import kotlinx.serialization.json.Json -@RiskFeature -fun createTelegramBotDefaultKtorCallRequestsFactories() = listOf( - SimpleRequestCallFactory(), - MultipartRequestCallFactory(), - DownloadFileRequestCallFactory, - DownloadFileChannelRequestCallFactory -) - -class KtorRequestsExecutor( +/** + * Represents default [BaseRequestsExecutor] working on [Ktor](https://ktor.io) [HttpClient] + * + * * On JS, JVM and MingwX64 platforms it is [dev.inmo.tgbotapi.bot.ktor.base.DefaultKtorRequestsExecutor] + * * On LinuxX64 it is [dev.inmo.tgbotapi.bot.ktor.base.MultipleClientKtorRequestsExecutor] + */ +expect class KtorRequestsExecutor ( telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper, client: HttpClient = HttpClient(), callsFactories: List = emptyList(), excludeDefaultFactories: Boolean = false, - private val requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter, - private val jsonFormatter: Json = nonstrictJsonFormat, - private val pipelineStepsHolder: KtorPipelineStepsHolder = KtorPipelineStepsHolder -) : BaseRequestsExecutor(telegramAPIUrlsKeeper) { - private val callsFactories: List = callsFactories.run { - if (!excludeDefaultFactories) { - this + createTelegramBotDefaultKtorCallRequestsFactories() - } else { - this - } - } - - private val client = client.config { - if (client.pluginOrNull(HttpTimeout) == null) { - install(HttpTimeout) - } - } - - override suspend fun execute(request: Request): T { - return runCatchingSafely { - pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories) - requestsLimiter.limit(request) { - var result: T? = null - lateinit var factoryHandledRequest: KtorCallFactory - for (potentialFactory in callsFactories) { - pipelineStepsHolder.onBeforeCallFactoryMakeCall(request, potentialFactory) - result = potentialFactory.makeCall( - client, - telegramAPIUrlsKeeper, - request, - jsonFormatter - ) - result = pipelineStepsHolder.onAfterCallFactoryMakeCall(result, request, potentialFactory) - if (result != null) { - factoryHandledRequest = potentialFactory - break - } - } - - result ?.let { - pipelineStepsHolder.onRequestResultPresented(it, request, factoryHandledRequest, callsFactories) - } ?: pipelineStepsHolder.onRequestResultAbsent(request, callsFactories) ?: error("Can't execute request: $request") - } - }.let { - val result = it.exceptionOrNull() ?.let { e -> - pipelineStepsHolder.onRequestException(request, e) ?.let { return@let it } - - when (e) { - is ClientRequestException -> { - val exceptionResult = runCatchingSafely { - val content = e.response.bodyAsText() - val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) - newRequestException( - responseObject, - content, - "Can't get result object from $content" - ) - } - exceptionResult.exceptionOrNull() ?.let { - CommonBotException(cause = e) - } ?: exceptionResult.getOrThrow() - } - is BotException -> e - else -> CommonBotException(cause = e) - } - } ?.let { Result.failure(it) } ?: it - pipelineStepsHolder.onRequestReturnResult(result, request, callsFactories) - } - } - - override fun close() { - client.close() - } -} - -class KtorRequestsExecutorBuilder( - var telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper -) { - var client: HttpClient = HttpClient() - var callsFactories: List = emptyList() - var excludeDefaultFactories: Boolean = false - var requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter - var jsonFormatter: Json = nonstrictJsonFormat - - fun build() = KtorRequestsExecutor(telegramAPIUrlsKeeper, client, callsFactories, excludeDefaultFactories, requestsLimiter, jsonFormatter) -} - -inline fun telegramBot( - telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper, - builder: KtorRequestsExecutorBuilder.() -> Unit = {} -): TelegramBot = KtorRequestsExecutorBuilder(telegramAPIUrlsKeeper).apply(builder).build() - -/** - * Shortcut for [telegramBot] - */ -@Suppress("NOTHING_TO_INLINE") -inline fun telegramBot( - token: String, - apiUrl: String = telegramBotAPIDefaultUrl, - testServer: Boolean = false, - builder: KtorRequestsExecutorBuilder.() -> Unit = {} -): TelegramBot = telegramBot(TelegramAPIUrlsKeeper(token, testServer, apiUrl), builder) + requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter, + jsonFormatter: Json = nonstrictJsonFormat, + pipelineStepsHolder: KtorPipelineStepsHolder = KtorPipelineStepsHolder +) : BaseRequestsExecutor diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutorFactories.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutorFactories.kt new file mode 100644 index 0000000000..695ecd989d --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutorFactories.kt @@ -0,0 +1,54 @@ +package dev.inmo.tgbotapi.bot.ktor + +import dev.inmo.tgbotapi.bot.BaseRequestsExecutor +import dev.inmo.tgbotapi.bot.TelegramBot +import dev.inmo.tgbotapi.bot.ktor.base.* +import dev.inmo.tgbotapi.bot.settings.limiters.ExceptionsOnlyLimiter +import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter +import dev.inmo.tgbotapi.utils.* +import io.ktor.client.HttpClient +import kotlinx.serialization.json.Json + +@RiskFeature +fun createTelegramBotDefaultKtorCallRequestsFactories() = listOf( + SimpleRequestCallFactory(), + MultipartRequestCallFactory(), + DownloadFileRequestCallFactory, + DownloadFileChannelRequestCallFactory +) + +class KtorRequestsExecutorBuilder( + var telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper +) { + var client: HttpClient = HttpClient() + var callsFactories: List = emptyList() + var excludeDefaultFactories: Boolean = false + var requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter + var jsonFormatter: Json = nonstrictJsonFormat + + fun build() = KtorRequestsExecutor( + telegramAPIUrlsKeeper, + client, + callsFactories, + excludeDefaultFactories, + requestsLimiter, + jsonFormatter + ) +} + +inline fun telegramBot( + telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper, + builder: KtorRequestsExecutorBuilder.() -> Unit = {} +): TelegramBot = KtorRequestsExecutorBuilder(telegramAPIUrlsKeeper).apply(builder).build() + +/** + * Shortcut for [telegramBot] + */ +@Suppress("NOTHING_TO_INLINE") +inline fun telegramBot( + token: String, + apiUrl: String = telegramBotAPIDefaultUrl, + testServer: Boolean = false, + builder: KtorRequestsExecutorBuilder.() -> Unit = {} +): TelegramBot = telegramBot(TelegramAPIUrlsKeeper(token, testServer, apiUrl), builder) + diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/AbstractRequestCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/AbstractRequestCallFactory.kt index f4e049703e..5d6346236e 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/AbstractRequestCallFactory.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/AbstractRequestCallFactory.kt @@ -1,6 +1,6 @@ package dev.inmo.tgbotapi.bot.ktor.base -import dev.inmo.micro_utils.coroutines.safelyWithResult +import dev.inmo.micro_utils.coroutines.runCatchingSafely import dev.inmo.tgbotapi.bot.ktor.KtorCallFactory import dev.inmo.tgbotapi.bot.exceptions.newRequestException import dev.inmo.tgbotapi.requests.GetUpdates @@ -56,7 +56,7 @@ abstract class AbstractRequestCallFactory : KtorCallFactory { val content = response.bodyAsText() val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) - return safelyWithResult { + return runCatchingSafely { (responseObject.result?.let { jsonFormatter.decodeFromJsonElement(request.resultDeserializer, it) } ?: response.let { diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/DefaultKtorRequestsExecutor.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/DefaultKtorRequestsExecutor.kt new file mode 100644 index 0000000000..3051931ec2 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/DefaultKtorRequestsExecutor.kt @@ -0,0 +1,100 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +import dev.inmo.micro_utils.coroutines.runCatchingSafely +import dev.inmo.tgbotapi.bot.BaseRequestsExecutor +import dev.inmo.tgbotapi.bot.exceptions.BotException +import dev.inmo.tgbotapi.bot.exceptions.CommonBotException +import dev.inmo.tgbotapi.bot.exceptions.newRequestException +import dev.inmo.tgbotapi.bot.ktor.KtorCallFactory +import dev.inmo.tgbotapi.bot.ktor.KtorPipelineStepsHolder +import dev.inmo.tgbotapi.bot.ktor.createTelegramBotDefaultKtorCallRequestsFactories +import dev.inmo.tgbotapi.bot.settings.limiters.ExceptionsOnlyLimiter +import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter +import dev.inmo.tgbotapi.requests.abstracts.Request +import dev.inmo.tgbotapi.types.Response +import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper +import dev.inmo.tgbotapi.utils.nonstrictJsonFormat +import io.ktor.client.* +import io.ktor.client.plugins.* +import io.ktor.client.statement.* +import kotlinx.serialization.json.Json + +class DefaultKtorRequestsExecutor( + telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper, + client: HttpClient = HttpClient(), + callsFactories: List = emptyList(), + excludeDefaultFactories: Boolean = false, + private val requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter, + private val jsonFormatter: Json = nonstrictJsonFormat, + private val pipelineStepsHolder: KtorPipelineStepsHolder = KtorPipelineStepsHolder +) : BaseRequestsExecutor(telegramAPIUrlsKeeper) { + private val callsFactories: List = callsFactories.run { + if (!excludeDefaultFactories) { + this + createTelegramBotDefaultKtorCallRequestsFactories() + } else { + this + } + } + + private val client = client.config { + if (client.pluginOrNull(HttpTimeout) == null) { + install(HttpTimeout) + } + } + + override suspend fun execute(request: Request): T { + return runCatchingSafely { + pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories) + requestsLimiter.limit(request) { + var result: T? = null + lateinit var factoryHandledRequest: KtorCallFactory + for (potentialFactory in callsFactories) { + pipelineStepsHolder.onBeforeCallFactoryMakeCall(request, potentialFactory) + result = potentialFactory.makeCall( + client, + telegramAPIUrlsKeeper, + request, + jsonFormatter + ) + result = pipelineStepsHolder.onAfterCallFactoryMakeCall(result, request, potentialFactory) + if (result != null) { + factoryHandledRequest = potentialFactory + break + } + } + + result ?.let { + pipelineStepsHolder.onRequestResultPresented(it, request, factoryHandledRequest, callsFactories) + } ?: pipelineStepsHolder.onRequestResultAbsent(request, callsFactories) ?: error("Can't execute request: $request") + } + }.let { + val result = it.exceptionOrNull() ?.let { e -> + pipelineStepsHolder.onRequestException(request, e) ?.let { return@let it } + + when (e) { + is ClientRequestException -> { + val exceptionResult = runCatchingSafely { + val content = e.response.bodyAsText() + val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) + newRequestException( + responseObject, + content, + "Can't get result object from $content" + ) + } + exceptionResult.exceptionOrNull() ?.let { + CommonBotException(cause = e) + } ?: exceptionResult.getOrThrow() + } + is BotException -> e + else -> CommonBotException(cause = e) + } + } ?.let { Result.failure(it) } ?: it + pipelineStepsHolder.onRequestReturnResult(result, request, callsFactories) + } + } + + override fun close() { + client.close() + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/MultipartRequestCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/MultipartRequestCallFactory.kt index 0fdcd1c5af..b01f66e2dd 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/MultipartRequestCallFactory.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/MultipartRequestCallFactory.kt @@ -4,8 +4,7 @@ import dev.inmo.tgbotapi.requests.abstracts.* import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper import dev.inmo.tgbotapi.utils.mapWithCommonValues import io.ktor.client.HttpClient -import io.ktor.client.request.forms.MultiPartFormDataContent -import io.ktor.client.request.forms.formData +import io.ktor.client.request.forms.* import io.ktor.http.Headers import io.ktor.http.HttpHeaders diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/MultipleClientKtorRequestsExecutor.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/MultipleClientKtorRequestsExecutor.kt new file mode 100644 index 0000000000..c8c6a73337 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/MultipleClientKtorRequestsExecutor.kt @@ -0,0 +1,118 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +import dev.inmo.micro_utils.coroutines.runCatchingSafely +import dev.inmo.tgbotapi.bot.BaseRequestsExecutor +import dev.inmo.tgbotapi.bot.ktor.KtorCallFactory +import dev.inmo.tgbotapi.bot.ktor.KtorPipelineStepsHolder +import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter +import dev.inmo.tgbotapi.requests.abstracts.Request +import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper +import io.ktor.client.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.Json + +/** + * This function is used in default constructor of [MultipleClientKtorRequestsExecutor] and on all non-native + * platforms and MingwX64 should return [client] + * + * On LinuxX64 it will create copy with Curl engine or throw an exception if engine is different with Curl + * + * @throws IllegalArgumentException When pass non Curl-based [HttpClient] on LinuxX64 + */ +internal expect inline fun platformClientCopy(client: HttpClient): HttpClient + +/** + * Will use its parameters of constructor to create several [DefaultKtorRequestsExecutor] and use them in [execute] + * and [close] operations + * + * This [BaseRequestsExecutor] has been created for LinuxX64 target due to its inability of requests paralleling + * + * Under the hood on each [execute] it will take [DefaultKtorRequestsExecutor] and mark it as busy, execute + * [Request], free up taken [DefaultKtorRequestsExecutor] and return (or throw) the result of execution + * + * @param requestExecutorsCount Amount of [DefaultKtorRequestsExecutor] which will be created and used under the + * hood + */ +class MultipleClientKtorRequestsExecutor ( + telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper, + callsFactories: List, + excludeDefaultFactories: Boolean, + requestsLimiter: RequestLimiter, + jsonFormatter: Json, + pipelineStepsHolder: KtorPipelineStepsHolder, + requestExecutorsCount: Int, + clientFactory: () -> HttpClient +) : BaseRequestsExecutor(telegramAPIUrlsKeeper) { + private val requestExecutors = (0 until requestExecutorsCount).map { + DefaultKtorRequestsExecutor( + telegramAPIUrlsKeeper, + clientFactory(), + callsFactories, + excludeDefaultFactories, + requestsLimiter, + jsonFormatter, + pipelineStepsHolder + ) + }.toSet() + private val freeClients = MutableStateFlow>(requestExecutors) + private val clientAllocationMutex = Mutex() + private val takerFlow = freeClients.mapNotNull { + clientAllocationMutex.withLock { + freeClients.value.firstOrNull() ?.also { + freeClients.value -= it + } ?: return@mapNotNull null + } + } + + constructor( + telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper, + client: HttpClient, + callsFactories: List, + excludeDefaultFactories: Boolean, + requestsLimiter: RequestLimiter, + jsonFormatter: Json, + pipelineStepsHolder: KtorPipelineStepsHolder + ) : this( + telegramAPIUrlsKeeper, + callsFactories, + excludeDefaultFactories, + requestsLimiter, + jsonFormatter, + pipelineStepsHolder, + client.engineConfig.threadsCount, + { platformClientCopy(client) } + ) + + private suspend fun prepareRequestsExecutor(): DefaultKtorRequestsExecutor { + return takerFlow.first() + } + + private suspend fun freeRequestsExecutor(client: DefaultKtorRequestsExecutor) { + clientAllocationMutex.withLock { + freeClients.value += client + } + } + + private suspend fun withRequestExecutor(block: suspend (DefaultKtorRequestsExecutor) -> T): T { + val requestsExecutor = prepareRequestsExecutor() + val result = runCatchingSafely { + block(requestsExecutor) + } + freeRequestsExecutor(requestsExecutor) + return result.getOrThrow() + } + + override suspend fun execute(request: Request): T = withRequestExecutor { + it.execute(request) + } + + override fun close() { + requestExecutors.forEach { + it.close() + } + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/ExceptionsOnlyLimiter.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/ExceptionsOnlyLimiter.kt index 41b7b002b6..1f6965f259 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/ExceptionsOnlyLimiter.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/settings/limiters/ExceptionsOnlyLimiter.kt @@ -1,18 +1,27 @@ package dev.inmo.tgbotapi.bot.settings.limiters +import dev.inmo.micro_utils.coroutines.runCatchingSafely import dev.inmo.tgbotapi.bot.exceptions.TooMuchRequestsException import kotlinx.coroutines.delay /** - * Simple limiter which will lock any request when [TooMuchRequestsExceptions] is thrown and rerun request after lock time + * Simple limiter which will lock any request when [TooMuchRequestsException] is thrown and rerun request after lock time */ object ExceptionsOnlyLimiter : RequestLimiter { override suspend fun limit(block: suspend () -> T): T { - return try { - block() - } catch (e: TooMuchRequestsException) { - delay(e.retryAfter.leftToRetry) - limit(block) + var result: Result? = null + while (result == null || result.isFailure) { + result = runCatchingSafely { + block() + }.onFailure { + it.printStackTrace() + if (it is TooMuchRequestsException) { + delay(it.retryAfter.leftToRetry) + } else { + throw it + } + } } + return result.getOrThrow() } } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/get/GetChat.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/get/GetChat.kt index 913c083500..7e929880c9 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/get/GetChat.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/chat/get/GetChat.kt @@ -19,7 +19,7 @@ data class GetChat( override val resultDeserializer: DeserializationStrategy = if (chatId is ChatIdWithThreadId) { ExtendedChatSerializer.BasedOnForumThread(chatId.threadId) } else { - ExtendedChatSerializer + ExtendedChatSerializer.Companion } override val requestSerializer: SerializationStrategy<*> get() = serializer() diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ExtendedAbstracts.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ExtendedAbstracts.kt index 734b8bacc3..9d2bc0bcbe 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ExtendedAbstracts.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/chat/ExtendedAbstracts.kt @@ -5,17 +5,17 @@ import dev.inmo.tgbotapi.types.message.abstracts.Message import dev.inmo.tgbotapi.types.message.abstracts.TelegramBotAPIMessageDeserializeOnlySerializer import kotlinx.serialization.Serializable -@Serializable(ExtendedChatSerializer::class) +@Serializable(ExtendedChatSerializer.Companion::class) sealed interface ExtendedChannelChat : ChannelChat, ExtendedPublicChat, ExtendedChatWithUsername { val linkedGroupChatId: IdChatIdentifier? } -@Serializable(ExtendedChatSerializer::class) +@Serializable(ExtendedChatSerializer.Companion::class) sealed interface ExtendedGroupChat : GroupChat, ExtendedPublicChat { val permissions: ChatPermissions } -@Serializable(ExtendedChatSerializer::class) +@Serializable(ExtendedChatSerializer.Companion::class) sealed interface ExtendedPrivateChat : PrivateChat, ExtendedChatWithUsername { val bio: String val hasPrivateForwards: Boolean @@ -34,7 +34,7 @@ sealed interface ExtendedPublicChat : ExtendedChat, PublicChat { val membersHidden: Boolean } -@Serializable(ExtendedChatSerializer::class) +@Serializable(ExtendedChatSerializer.Companion::class) sealed interface ExtendedSupergroupChat : SupergroupChat, ExtendedGroupChat, ExtendedChatWithUsername { val slowModeDelay: Long? val stickerSetName: StickerSetName? @@ -58,15 +58,15 @@ sealed interface ExtendedSupergroupChat : SupergroupChat, ExtendedGroupChat, Ext val isAggressiveAntiSpamEnabled: Boolean } -@Serializable(ExtendedChatSerializer::class) +@Serializable(ExtendedChatSerializer.Companion::class) sealed interface ExtendedForumChat : ExtendedSupergroupChat, ForumChat -@Serializable(ExtendedChatSerializer::class) +@Serializable(ExtendedChatSerializer.Companion::class) sealed interface ExtendedChat : Chat { val chatPhoto: ChatPhoto? } -@Serializable(ExtendedChatSerializer::class) +@Serializable(ExtendedChatSerializer.Companion::class) sealed interface ExtendedChatWithUsername : UsernameChat, ExtendedChat { val activeUsernames: List } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/TextSourceSerializer.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/TextSourceSerializer.kt index e93d87f575..7ba5856e02 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/TextSourceSerializer.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/TextSourceSerializer.kt @@ -1,37 +1,78 @@ package dev.inmo.tgbotapi.types.message.textsources +import dev.inmo.micro_utils.serialization.mapper.MapperSerializer import dev.inmo.micro_utils.serialization.typed_serializer.TypedSerializer import kotlinx.serialization.KSerializer +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder -private val baseSerializers: Map> = mapOf( - "regular" to RegularTextSource.serializer(), - "text_link" to TextLinkTextSource.serializer(), - "code" to CodeTextSource.serializer(), - "url" to URLTextSource.serializer(), - "pre" to PreTextSource.serializer(), - "bot_command" to BotCommandTextSource.serializer(), - "strikethrough" to StrikethroughTextSource.serializer(), - "italic" to ItalicTextSource.serializer(), - "bold" to BoldTextSource.serializer(), - "email" to EMailTextSource.serializer(), - "underline" to UnderlineTextSource.serializer(), - "mention" to MentionTextSource.serializer(), - "phone_number" to PhoneNumberTextSource.serializer(), - "text_mention" to TextMentionTextSource.serializer(), - "hashtag" to HashTagTextSource.serializer(), - "cashtag" to CashTagTextSource.serializer(), - "spoiler" to SpoilerTextSource.serializer(), - "custom_emoji" to CustomEmojiTextSource.serializer(), -) +//private val baseSerializers: Map> = mapOf( +// "regular" to RegularTextSource.serializer(), +// "text_link" to TextLinkTextSource.serializer(), +// "code" to CodeTextSource.serializer(), +// "url" to URLTextSource.serializer(), +// "pre" to PreTextSource.serializer(), +// "bot_command" to BotCommandTextSource.serializer(), +// "strikethrough" to StrikethroughTextSource.serializer(), +// "italic" to ItalicTextSource.serializer(), +// "bold" to BoldTextSource.serializer(), +// "email" to EMailTextSource.serializer(), +// "underline" to UnderlineTextSource.serializer(), +// "mention" to MentionTextSource.serializer(), +// "phone_number" to PhoneNumberTextSource.serializer(), +// "text_mention" to TextMentionTextSource.serializer(), +// "hashtag" to HashTagTextSource.serializer(), +// "cashtag" to CashTagTextSource.serializer(), +// "spoiler" to SpoilerTextSource.serializer(), +// "custom_emoji" to CustomEmojiTextSource.serializer(), +//) + +object TextSourceSerializer : TypedSerializer(TextSource::class, emptyMap()) { + private val baseSerializers: Map> by lazy { + mapOf( + "regular" to RegularTextSource.serializer(), + "text_link" to TextLinkTextSource.serializer(), + "code" to CodeTextSource.serializer(), + "url" to URLTextSource.serializer(), + "pre" to PreTextSource.serializer(), + "bot_command" to BotCommandTextSource.serializer(), + "strikethrough" to StrikethroughTextSource.serializer(), + "italic" to ItalicTextSource.serializer(), + "bold" to BoldTextSource.serializer(), + "email" to EMailTextSource.serializer(), + "underline" to UnderlineTextSource.serializer(), + "mention" to MentionTextSource.serializer(), + "phone_number" to PhoneNumberTextSource.serializer(), + "text_mention" to TextMentionTextSource.serializer(), + "hashtag" to HashTagTextSource.serializer(), + "cashtag" to CashTagTextSource.serializer(), + "spoiler" to SpoilerTextSource.serializer(), + "custom_emoji" to CustomEmojiTextSource.serializer(), + ).also { + it.forEach { (k, s) -> + include(k, s) + } + } + } + + override fun serialize(encoder: Encoder, value: TextSource) { + baseSerializers // init base serializers + super.serialize(encoder, value) + } + + override fun deserialize(decoder: Decoder): TextSource { + baseSerializers // init base serializers + return super.deserialize(decoder) + } -object TextSourceSerializer : TypedSerializer(TextSource::class, baseSerializers) { override fun include(type: String, serializer: KSerializer) { - require(type !in baseSerializers.keys) + require(type !in serializers.keys) super.include(type, serializer) } override fun exclude(type: String) { - require(type !in baseSerializers.keys) + require(type !in serializers.keys) super.exclude(type) } } diff --git a/tgbotapi.core/src/jsMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt b/tgbotapi.core/src/jsMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt new file mode 100644 index 0000000000..fd842f95bb --- /dev/null +++ b/tgbotapi.core/src/jsMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt @@ -0,0 +1,5 @@ +package dev.inmo.tgbotapi.bot.ktor + +import dev.inmo.tgbotapi.bot.ktor.base.DefaultKtorRequestsExecutor + +actual typealias KtorRequestsExecutor = DefaultKtorRequestsExecutor diff --git a/tgbotapi.core/src/jsMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt b/tgbotapi.core/src/jsMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt new file mode 100644 index 0000000000..3b3451a931 --- /dev/null +++ b/tgbotapi.core/src/jsMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt @@ -0,0 +1,13 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +import io.ktor.client.* + +/** + * This function is used in default constructor of [MultipleClientKtorRequestsExecutor] and on all non-native + * platforms and MingwX64 should return [client] + * + * On LinuxX64 it will create copy with Curl engine or throw an exception if engine is different with Curl + * + * @throws IllegalArgumentException When pass non Curl-based [HttpClient] on LinuxX64 + */ +internal actual inline fun platformClientCopy(client: HttpClient): HttpClient = client.config { } diff --git a/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt new file mode 100644 index 0000000000..fd842f95bb --- /dev/null +++ b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt @@ -0,0 +1,5 @@ +package dev.inmo.tgbotapi.bot.ktor + +import dev.inmo.tgbotapi.bot.ktor.base.DefaultKtorRequestsExecutor + +actual typealias KtorRequestsExecutor = DefaultKtorRequestsExecutor diff --git a/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt new file mode 100644 index 0000000000..3b3451a931 --- /dev/null +++ b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt @@ -0,0 +1,13 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +import io.ktor.client.* + +/** + * This function is used in default constructor of [MultipleClientKtorRequestsExecutor] and on all non-native + * platforms and MingwX64 should return [client] + * + * On LinuxX64 it will create copy with Curl engine or throw an exception if engine is different with Curl + * + * @throws IllegalArgumentException When pass non Curl-based [HttpClient] on LinuxX64 + */ +internal actual inline fun platformClientCopy(client: HttpClient): HttpClient = client.config { } diff --git a/tgbotapi.core/src/linuxX64Main/kotlin/PackageInfo.kt b/tgbotapi.core/src/linuxX64Main/kotlin/PackageInfo.kt new file mode 100644 index 0000000000..20be3dda7a --- /dev/null +++ b/tgbotapi.core/src/linuxX64Main/kotlin/PackageInfo.kt @@ -0,0 +1 @@ +package dev.inmo.tgbotapi diff --git a/tgbotapi.core/src/linuxX64Main/kotlin/bot/ktor/KtorRequestsExecutor.kt b/tgbotapi.core/src/linuxX64Main/kotlin/bot/ktor/KtorRequestsExecutor.kt new file mode 100644 index 0000000000..2f49bfc213 --- /dev/null +++ b/tgbotapi.core/src/linuxX64Main/kotlin/bot/ktor/KtorRequestsExecutor.kt @@ -0,0 +1,5 @@ +package dev.inmo.tgbotapi.bot.ktor + +import dev.inmo.tgbotapi.bot.ktor.base.MultipleClientKtorRequestsExecutor + +actual typealias KtorRequestsExecutor = MultipleClientKtorRequestsExecutor diff --git a/tgbotapi.core/src/linuxX64Main/kotlin/bot/ktor/base/ActualPlatformClientCopy.kt b/tgbotapi.core/src/linuxX64Main/kotlin/bot/ktor/base/ActualPlatformClientCopy.kt new file mode 100644 index 0000000000..8f7104ddc0 --- /dev/null +++ b/tgbotapi.core/src/linuxX64Main/kotlin/bot/ktor/base/ActualPlatformClientCopy.kt @@ -0,0 +1,22 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +import io.ktor.client.* +import io.ktor.client.engine.curl.* + +/** + * This function is used in default constructor of [MultipleClientKtorRequestsExecutor] and on all non-native + * platforms and MingwX64 should return [client] + * + * On LinuxX64 it will create copy with Curl engine or throw an exception if engine is different with Curl + * + * @throws IllegalArgumentException When pass non Curl-based [HttpClient] on LinuxX64 + */ +internal actual inline fun platformClientCopy(client: HttpClient): HttpClient = (client.engineConfig as? CurlClientEngineConfig) ?.let { + lateinit var config: HttpClientConfig + client.config { + config = this as HttpClientConfig + }.close() + HttpClient(Curl) { + this.plusAssign(config) + } +} ?: throw IllegalArgumentException("On LinuxX64 TelegramBotAPI currently support only Curl Ktor HttpClient engine") diff --git a/tgbotapi.core/src/linuxX64Main/kotlin/requests/abstracts/ActualMPPFileAsMultipartFile.kt b/tgbotapi.core/src/linuxX64Main/kotlin/requests/abstracts/ActualMPPFileAsMultipartFile.kt new file mode 100644 index 0000000000..942cbfddf2 --- /dev/null +++ b/tgbotapi.core/src/linuxX64Main/kotlin/requests/abstracts/ActualMPPFileAsMultipartFile.kt @@ -0,0 +1,9 @@ +package dev.inmo.tgbotapi.requests.abstracts + +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.ktor.common.input +import dev.inmo.tgbotapi.requests.abstracts.MultipartFile + +actual fun MPPFile.asMultipartFile(): MultipartFile = MultipartFile(this.name) { + input() +} diff --git a/tgbotapi.core/src/linuxX64Main/kotlin/utils/ActualByteReadChannelAsInput.kt b/tgbotapi.core/src/linuxX64Main/kotlin/utils/ActualByteReadChannelAsInput.kt new file mode 100644 index 0000000000..8f52f52a4e --- /dev/null +++ b/tgbotapi.core/src/linuxX64Main/kotlin/utils/ActualByteReadChannelAsInput.kt @@ -0,0 +1,7 @@ +package dev.inmo.tgbotapi.utils + +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.core.Input +import io.ktor.utils.io.readRemaining + +actual suspend fun ByteReadChannel.asInput(): Input = readRemaining() diff --git a/tgbotapi.core/src/linuxX64Main/kotlin/utils/ActualMimeType.kt b/tgbotapi.core/src/linuxX64Main/kotlin/utils/ActualMimeType.kt new file mode 100644 index 0000000000..4158408546 --- /dev/null +++ b/tgbotapi.core/src/linuxX64Main/kotlin/utils/ActualMimeType.kt @@ -0,0 +1,11 @@ +package dev.inmo.tgbotapi.utils + +import kotlinx.serialization.Serializable + +//actual typealias MimeType = MimeType + +@Serializable(MimeTypeSerializer::class) +actual data class MimeType( + actual val raw: String +) +internal actual fun createMimeType(raw: String): MimeType = MimeType(raw) diff --git a/tgbotapi.core/src/mingwX64Main/kotlin/PackageInfo.kt b/tgbotapi.core/src/mingwX64Main/kotlin/PackageInfo.kt new file mode 100644 index 0000000000..20be3dda7a --- /dev/null +++ b/tgbotapi.core/src/mingwX64Main/kotlin/PackageInfo.kt @@ -0,0 +1 @@ +package dev.inmo.tgbotapi diff --git a/tgbotapi.core/src/mingwX64Main/kotlin/bot/ktor/KtorRequestsExecutor.kt b/tgbotapi.core/src/mingwX64Main/kotlin/bot/ktor/KtorRequestsExecutor.kt new file mode 100644 index 0000000000..fd842f95bb --- /dev/null +++ b/tgbotapi.core/src/mingwX64Main/kotlin/bot/ktor/KtorRequestsExecutor.kt @@ -0,0 +1,5 @@ +package dev.inmo.tgbotapi.bot.ktor + +import dev.inmo.tgbotapi.bot.ktor.base.DefaultKtorRequestsExecutor + +actual typealias KtorRequestsExecutor = DefaultKtorRequestsExecutor diff --git a/tgbotapi.core/src/mingwX64Main/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt b/tgbotapi.core/src/mingwX64Main/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt new file mode 100644 index 0000000000..05eb9ece47 --- /dev/null +++ b/tgbotapi.core/src/mingwX64Main/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt @@ -0,0 +1,14 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +import io.ktor.client.* +import io.ktor.client.engine.winhttp.* + +/** + * This function is used in default constructor of [MultipleClientKtorRequestsExecutor] and on all non-native + * platforms and MingwX64 should return [client] + * + * On LinuxX64 it will create copy with Curl engine or throw an exception if engine is different with Curl + * + * @throws IllegalArgumentException When pass non Curl-based [HttpClient] on LinuxX64 + */ +internal actual inline fun platformClientCopy(client: HttpClient): HttpClient = client.config { } diff --git a/tgbotapi.core/src/mingwX64Main/kotlin/requests/abstracts/ActualMPPFileAsMultipartFile.kt b/tgbotapi.core/src/mingwX64Main/kotlin/requests/abstracts/ActualMPPFileAsMultipartFile.kt new file mode 100644 index 0000000000..942cbfddf2 --- /dev/null +++ b/tgbotapi.core/src/mingwX64Main/kotlin/requests/abstracts/ActualMPPFileAsMultipartFile.kt @@ -0,0 +1,9 @@ +package dev.inmo.tgbotapi.requests.abstracts + +import dev.inmo.micro_utils.common.MPPFile +import dev.inmo.micro_utils.ktor.common.input +import dev.inmo.tgbotapi.requests.abstracts.MultipartFile + +actual fun MPPFile.asMultipartFile(): MultipartFile = MultipartFile(this.name) { + input() +} diff --git a/tgbotapi.core/src/mingwX64Main/kotlin/utils/ActualByteReadChannelAsInput.kt b/tgbotapi.core/src/mingwX64Main/kotlin/utils/ActualByteReadChannelAsInput.kt new file mode 100644 index 0000000000..8f52f52a4e --- /dev/null +++ b/tgbotapi.core/src/mingwX64Main/kotlin/utils/ActualByteReadChannelAsInput.kt @@ -0,0 +1,7 @@ +package dev.inmo.tgbotapi.utils + +import io.ktor.utils.io.ByteReadChannel +import io.ktor.utils.io.core.Input +import io.ktor.utils.io.readRemaining + +actual suspend fun ByteReadChannel.asInput(): Input = readRemaining() diff --git a/tgbotapi.core/src/mingwX64Main/kotlin/utils/ActualMimeType.kt b/tgbotapi.core/src/mingwX64Main/kotlin/utils/ActualMimeType.kt new file mode 100644 index 0000000000..4158408546 --- /dev/null +++ b/tgbotapi.core/src/mingwX64Main/kotlin/utils/ActualMimeType.kt @@ -0,0 +1,11 @@ +package dev.inmo.tgbotapi.utils + +import kotlinx.serialization.Serializable + +//actual typealias MimeType = MimeType + +@Serializable(MimeTypeSerializer::class) +actual data class MimeType( + actual val raw: String +) +internal actual fun createMimeType(raw: String): MimeType = MimeType(raw) diff --git a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCastsNew.kt b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCastsNew.kt index 7010113283..b8f5a82e4f 100644 --- a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCastsNew.kt +++ b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/ClassCastsNew.kt @@ -160,6 +160,7 @@ import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMember import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMemberImpl import dev.inmo.tgbotapi.types.chat.member.BannedChatMember import dev.inmo.tgbotapi.types.chat.member.ChatMember +import dev.inmo.tgbotapi.types.chat.member.ChatMemberUpdated import dev.inmo.tgbotapi.types.chat.member.KickedChatMember import dev.inmo.tgbotapi.types.chat.member.LeftChatMember import dev.inmo.tgbotapi.types.chat.member.LeftChatMemberImpl @@ -659,6 +660,15 @@ public inline fun WithUser.chatMemberOrThrow(): ChatMember = this as public inline fun WithUser.ifChatMember(block: (ChatMember) -> T): T? = chatMemberOrNull() ?.let(block) +public inline fun WithUser.chatMemberUpdatedOrNull(): ChatMemberUpdated? = this as? + dev.inmo.tgbotapi.types.chat.member.ChatMemberUpdated + +public inline fun WithUser.chatMemberUpdatedOrThrow(): ChatMemberUpdated = this as + dev.inmo.tgbotapi.types.chat.member.ChatMemberUpdated + +public inline fun WithUser.ifChatMemberUpdated(block: (ChatMemberUpdated) -> T): T? = + chatMemberUpdatedOrNull() ?.let(block) + public inline fun WithUser.kickedChatMemberOrNull(): KickedChatMember? = this as? dev.inmo.tgbotapi.types.chat.member.KickedChatMember diff --git a/tgbotapi.webapps/src/commonMain/kotlin/PackageInfo.kt b/tgbotapi.webapps/src/commonMain/kotlin/PackageInfo.kt new file mode 100644 index 0000000000..2f55f849f3 --- /dev/null +++ b/tgbotapi.webapps/src/commonMain/kotlin/PackageInfo.kt @@ -0,0 +1,5 @@ +/** + * This file has been created to fix problem with native targets which didn't compile empty project klib file. This problem + * leads to impossible project publishing + */ +package dev.inmo.tgbotapi.webapps diff --git a/tgbotapi/src/commonMain/kotlin/PackageInfo.kt b/tgbotapi/src/commonMain/kotlin/PackageInfo.kt new file mode 100644 index 0000000000..63b10775e7 --- /dev/null +++ b/tgbotapi/src/commonMain/kotlin/PackageInfo.kt @@ -0,0 +1,5 @@ +/** + * This file has been created to fix problem with native targets which didn't compile empty project klib file. This problem + * leads to impossible project publishing + */ +package dev.inmo.tgbotapi