diff --git a/.github/workflows/packages_publishing.yml b/.github/workflows/packages_publishing.yml index fe93346a45..c412b1cffb 100644 --- a/.github/workflows/packages_publishing.yml +++ b/.github/workflows/packages_publishing.yml @@ -14,6 +14,8 @@ jobs: 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 3297098158..c6781af65e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,17 @@ ## 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.6` + * `Kotlin`: `1.8.10` -> `1.8.20` + * `MicroUtils`: `0.17.5` -> `0.17.6` +* `Core`: + * New `RequestsExecutor` - `MultipleClientKtorRequestsExecutor` + * Old `KtorRequestsExecutor` has been renamed to `DefaultKtorRequestsExecutor` + * `KtorRequestsExecutor` now is `expect class` + * On `JS` and `JVM` platforms it is `DefaultKtorRequestsExecutor` + * On `LinuxX64` and `MinGWX64` platforms it is `MultipleClientKtorRequestsExecutor` ## 7.0.1 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c314432fb..b6283d6ad1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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/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..503601bc4b 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 and JVM platforms it is [dev.inmo.tgbotapi.bot.ktor.base.DefaultKtorRequestsExecutor] + * * On LinuxX64 and MingwX64 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/MultipleClientKtorRequestsExecutor.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/MultipleClientKtorRequestsExecutor.kt new file mode 100644 index 0000000000..8a468606fc --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/MultipleClientKtorRequestsExecutor.kt @@ -0,0 +1,120 @@ +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 should return [HttpClient.config] call + * + * On LinuxX64 it will create copy with Curl engine or throw an exception if engine is different with Curl + * On MingwX64 it will create copy with WinHttp engine or throw an exception if engine is different with WinHttp + * + * @throws IllegalArgumentException When pass non Curl-based [HttpClient] on LinuxX64 or non WinHttp-based [HttpClient] + * on MingwX64 + */ +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 native targets due to their 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/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..998333d34f --- /dev/null +++ b/tgbotapi.core/src/jsMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt @@ -0,0 +1,15 @@ +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 should return [HttpClient.config] call + * + * On LinuxX64 it will create copy with Curl engine or throw an exception if engine is different with Curl + * On MingwX64 it will create copy with WinHttp engine or throw an exception if engine is different with WinHttp + * + * @throws IllegalArgumentException When pass non Curl-based [HttpClient] on LinuxX64 or non WinHttp-based [HttpClient] + * on MingwX64 + */ +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..998333d34f --- /dev/null +++ b/tgbotapi.core/src/jvmMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt @@ -0,0 +1,15 @@ +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 should return [HttpClient.config] call + * + * On LinuxX64 it will create copy with Curl engine or throw an exception if engine is different with Curl + * On MingwX64 it will create copy with WinHttp engine or throw an exception if engine is different with WinHttp + * + * @throws IllegalArgumentException When pass non Curl-based [HttpClient] on LinuxX64 or non WinHttp-based [HttpClient] + * on MingwX64 + */ +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..3e883a1ba7 --- /dev/null +++ b/tgbotapi.core/src/linuxX64Main/kotlin/bot/ktor/base/ActualPlatformClientCopy.kt @@ -0,0 +1,24 @@ +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 should return [client] + * + * On LinuxX64 it will create copy with Curl engine or throw an exception if engine is different with Curl + * On MingwX64 it will create copy with WinHttp engine or throw an exception if engine is different with WinHttp + * + * @throws IllegalArgumentException When pass non Curl-based [HttpClient] on LinuxX64 or non WinHttp-based [HttpClient] + * on MingwX64 + */ +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/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..2f49bfc213 --- /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.MultipleClientKtorRequestsExecutor + +actual typealias KtorRequestsExecutor = MultipleClientKtorRequestsExecutor 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..27b486a3a1 --- /dev/null +++ b/tgbotapi.core/src/mingwX64Main/kotlin/dev/inmo/tgbotapi/bot/ktor/base/ActualPlatformClientCopy.kt @@ -0,0 +1,24 @@ +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 should return [client] + * + * On LinuxX64 it will create copy with Curl engine or throw an exception if engine is different with Curl + * On MingwX64 it will create copy with WinHttp engine or throw an exception if engine is different with WinHttp + * + * @throws IllegalArgumentException When pass non Curl-based [HttpClient] on LinuxX64 or non WinHttp-based [HttpClient] + * on MingwX64 + */ +internal actual inline fun platformClientCopy(client: HttpClient): HttpClient = (client.engineConfig as? WinHttpClientEngineConfig) ?.let { engineConfig -> + lateinit var config: HttpClientConfig + client.config { + config = this as HttpClientConfig + }.close() + HttpClient(WinHttp) { + this.plusAssign(config) + } +} ?: throw IllegalArgumentException("On LinuxX64 TelegramBotAPI currently support only Curl Ktor HttpClient engine")