diff --git a/CHANGELOG.md b/CHANGELOG.md index 557e97c239..8c714c0369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ __This update contains including of [Telegram Bot API 6.0](https://core.telegram __All the `tgbotapi.extensions.*` packages have been removed__ * `Core`: + * **`Ktor` package renamed. Migration:** `dev.inmo.tgbotapi.bot.Ktor` -> `dev.inmo.tgbotapi.bot.ktor` * Constructor of `UnknownInlineKeyboardButton` is not internal and can be created with any `json` ([#563](https://github.com/InsanusMokrassar/TelegramBotAPI/issues/563)) * All the interfaces from `dev.inmo.tgbotapi.types.files.abstracts` have been replaced to `dev.inmo.tgbotapi.types.files` and converted to sealed ([#550](https://github.com/InsanusMokrassar/TelegramBotAPI/issues/550)) * `PassportFile` has been replaced to `dev.inmo.tgbotapi.types.files` diff --git a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/BotExtensions.kt b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/BotExtensions.kt index 4390ab619b..6f7dc422f9 100644 --- a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/BotExtensions.kt +++ b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/BotExtensions.kt @@ -1,6 +1,6 @@ package dev.inmo.tgbotapi.extensions.api -import dev.inmo.tgbotapi.bot.Ktor.telegramBot +import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper import dev.inmo.tgbotapi.utils.telegramBotAPIDefaultUrl 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 0c5b881580..1199669657 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 @@ -5,8 +5,8 @@ import dev.inmo.micro_utils.fsm.common.State import dev.inmo.micro_utils.fsm.common.StatesManager import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager import dev.inmo.micro_utils.fsm.common.managers.InMemoryDefaultStatesManagerRepo -import dev.inmo.tgbotapi.bot.Ktor.KtorRequestsExecutorBuilder -import dev.inmo.tgbotapi.bot.Ktor.telegramBot +import dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder +import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.startGettingOfUpdatesByLongPolling import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBot.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBot.kt index 6ab989cd08..fc446fdf36 100644 --- a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBot.kt +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/TelegramBot.kt @@ -1,8 +1,8 @@ package dev.inmo.tgbotapi.extensions.behaviour_builder import dev.inmo.micro_utils.coroutines.ExceptionHandler -import dev.inmo.tgbotapi.bot.Ktor.KtorRequestsExecutorBuilder -import dev.inmo.tgbotapi.bot.Ktor.telegramBot +import dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder +import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.extensions.utils.updates.retrieving.startGettingOfUpdatesByLongPolling import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/KtorCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/KtorCallFactory.kt index 10d89d744c..27bfcd91a4 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/KtorCallFactory.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/KtorCallFactory.kt @@ -1,16 +1,4 @@ package dev.inmo.tgbotapi.bot.Ktor -import dev.inmo.micro_utils.common.Optional -import dev.inmo.tgbotapi.requests.abstracts.Request -import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper -import io.ktor.client.HttpClient -import kotlinx.serialization.json.Json - -interface KtorCallFactory { - suspend fun makeCall( - client: HttpClient, - urlsKeeper: TelegramAPIUrlsKeeper, - request: Request, - jsonFormatter: Json - ): T? -} +@Deprecated("Replaced", ReplaceWith("KtorCallFactory", "dev.inmo.tgbotapi.bot.ktor.KtorCallFactory")) +typealias KtorCallFactory = dev.inmo.tgbotapi.bot.ktor.KtorCallFactory 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 1e7da06255..e74804e85a 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,127 +1,29 @@ package dev.inmo.tgbotapi.bot.Ktor -import dev.inmo.micro_utils.coroutines.safely -import dev.inmo.tgbotapi.bot.BaseRequestsExecutor -import dev.inmo.tgbotapi.bot.Ktor.base.* import dev.inmo.tgbotapi.bot.TelegramBot -import dev.inmo.tgbotapi.bot.exceptions.newRequestException -import dev.inmo.tgbotapi.bot.ktor.KtorPipelineStepsHolder -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.features.* -import io.ktor.client.statement.readText -import kotlinx.serialization.json.Json @RiskFeature -fun createTelegramBotDefaultKtorCallRequestsFactories() = listOf( - SimpleRequestCallFactory(), - MultipartRequestCallFactory(), - DownloadFileRequestCallFactory, - DownloadFileChannelRequestCallFactory -) +@Deprecated("Replaced", ReplaceWith("createTelegramBotDefaultKtorCallRequestsFactories", "dev.inmo.tgbotapi.bot.ktor.createTelegramBotDefaultKtorCallRequestsFactories")) +fun createTelegramBotDefaultKtorCallRequestsFactories() = dev.inmo.tgbotapi.bot.ktor.createTelegramBotDefaultKtorCallRequestsFactories() -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 - } - } +@Deprecated("Replaced", ReplaceWith("KtorRequestsExecutor", "dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutor")) +typealias KtorRequestsExecutor = dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutor - private val client = client.config { - if (client.feature(HttpTimeout) == null) { - install(HttpTimeout) - } - } - - override suspend fun execute(request: Request): T { - return runCatching { - safely( - { e -> - pipelineStepsHolder.onRequestException(request, e) ?.let { return@safely it } - - throw if (e is ClientRequestException) { - val content = e.response.readText() - val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) - newRequestException( - responseObject, - content, - "Can't get result object from $content" - ) - } else { - e - } - - } - ) { - pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories) - requestsLimiter.limit { - 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 { - pipelineStepsHolder.onRequestReturnResult(it, 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) -} +@Deprecated("Replaced", ReplaceWith("KtorRequestsExecutorBuilder", "dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder")) +typealias KtorRequestsExecutorBuilder = dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder +@Deprecated("telegramBot", ReplaceWith("createTelegramBotDefaultKtorCallRequestsFactories", "dev.inmo.tgbotapi.bot.ktor.telegramBot")) inline fun telegramBot( telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper, builder: KtorRequestsExecutorBuilder.() -> Unit = {} -): TelegramBot = KtorRequestsExecutorBuilder(telegramAPIUrlsKeeper).apply(builder).build() +): TelegramBot = dev.inmo.tgbotapi.bot.ktor.telegramBot(telegramAPIUrlsKeeper, builder) /** * Shortcut for [telegramBot] */ @Suppress("NOTHING_TO_INLINE") +@Deprecated("telegramBot", ReplaceWith("createTelegramBotDefaultKtorCallRequestsFactories", "dev.inmo.tgbotapi.bot.ktor.telegramBot")) inline fun telegramBot( token: String, apiUrl: String = telegramBotAPIDefaultUrl, 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 e62250f706..370bc110e7 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,80 +1,6 @@ package dev.inmo.tgbotapi.bot.Ktor.base -import dev.inmo.micro_utils.coroutines.safely -import dev.inmo.micro_utils.coroutines.safelyWithResult -import dev.inmo.tgbotapi.bot.Ktor.KtorCallFactory -import dev.inmo.tgbotapi.bot.exceptions.newRequestException -import dev.inmo.tgbotapi.requests.GetUpdates -import dev.inmo.tgbotapi.requests.abstracts.Request -import dev.inmo.tgbotapi.types.Response -import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper -import io.ktor.client.HttpClient -import io.ktor.client.call.receive -import io.ktor.client.features.timeout -import io.ktor.client.request.* -import io.ktor.client.statement.HttpResponse -import io.ktor.http.ContentType -import kotlinx.serialization.json.Json -import kotlin.collections.set +var defaultUpdateTimeoutForZeroDelay = dev.inmo.tgbotapi.bot.ktor.base.defaultUpdateTimeoutForZeroDelay -var defaultUpdateTimeoutForZeroDelay = 1000L - -abstract class AbstractRequestCallFactory : KtorCallFactory { - private val methodsCache: MutableMap = mutableMapOf() - override suspend fun makeCall( - client: HttpClient, - urlsKeeper: TelegramAPIUrlsKeeper, - request: Request, - jsonFormatter: Json - ): T? { - val preparedBody = prepareCallBody(client, urlsKeeper, request) ?: return null - - client.post { - url( - methodsCache[request.method()] ?: "${urlsKeeper.commonAPIUrl}/${request.method()}".also { - methodsCache[request.method()] = it - } - ) - accept(ContentType.Application.Json) - - if (request is GetUpdates) { - request.timeout?.times(1000L)?.let { customTimeoutMillis -> - if (customTimeoutMillis > 0) { - timeout { - requestTimeoutMillis = customTimeoutMillis - socketTimeoutMillis = customTimeoutMillis - } - } else { - timeout { - requestTimeoutMillis = defaultUpdateTimeoutForZeroDelay - socketTimeoutMillis = defaultUpdateTimeoutForZeroDelay - } - } - } - } - - body = preparedBody - }.let { response -> - val content = response.receive() - val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) - - return safelyWithResult { - (responseObject.result?.let { - jsonFormatter.decodeFromJsonElement(request.resultDeserializer, it) - } ?: response.let { - throw newRequestException( - responseObject, - content, - "Can't get result object from $content" - ) - }) - }.getOrThrow() - } - } - - protected abstract fun prepareCallBody( - client: HttpClient, - urlsKeeper: TelegramAPIUrlsKeeper, - request: Request - ): Any? -} +@Deprecated("Replaced", ReplaceWith("AbstractRequestCallFactory", "dev.inmo.tgbotapi.bot.ktor.base.AbstractRequestCallFactory")) +typealias AbstractRequestCallFactory = dev.inmo.tgbotapi.bot.ktor.base.AbstractRequestCallFactory diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/DownloadFileChannelRequestCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/DownloadFileChannelRequestCallFactory.kt index 46cf0c1aec..56534bc0ad 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/DownloadFileChannelRequestCallFactory.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/DownloadFileChannelRequestCallFactory.kt @@ -1,42 +1,4 @@ package dev.inmo.tgbotapi.bot.Ktor.base -import dev.inmo.micro_utils.coroutines.* -import dev.inmo.tgbotapi.bot.Ktor.KtorCallFactory -import dev.inmo.tgbotapi.requests.DownloadFileStream -import dev.inmo.tgbotapi.requests.abstracts.Request -import dev.inmo.tgbotapi.utils.ByteReadChannelAllocator -import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper -import io.ktor.client.HttpClient -import io.ktor.client.call.receive -import io.ktor.client.request.get -import io.ktor.client.statement.HttpStatement -import io.ktor.utils.io.* -import kotlinx.coroutines.* -import kotlinx.serialization.json.Json -import kotlin.coroutines.coroutineContext - -object DownloadFileChannelRequestCallFactory : KtorCallFactory { - override suspend fun makeCall( - client: HttpClient, - urlsKeeper: TelegramAPIUrlsKeeper, - request: Request, - jsonFormatter: Json - ): T? = (request as? DownloadFileStream) ?.let { - val fullUrl = urlsKeeper.createFileLinkUrl(it.filePath) - - ByteReadChannelAllocator { - val scope = CoroutineScope(currentCoroutineContext() + SupervisorJob()) - val outChannel = ByteChannel() - scope.launch { - runCatchingSafely { - client.get(fullUrl).execute { httpResponse -> - val channel: ByteReadChannel = httpResponse.receive() - channel.copyAndClose(outChannel) - } - } - scope.cancel() - } - outChannel - } as T - } -} +@Deprecated("Replaced", ReplaceWith("DownloadFileChannelRequestCallFactory", "dev.inmo.tgbotapi.bot.ktor.base.DownloadFileRequestCallFactory")) +typealias DownloadFileChannelRequestCallFactory = dev.inmo.tgbotapi.bot.ktor.base.DownloadFileRequestCallFactory diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/DownloadFileRequestCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/DownloadFileRequestCallFactory.kt index bf8e3687d9..5eabacad94 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/DownloadFileRequestCallFactory.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/DownloadFileRequestCallFactory.kt @@ -1,26 +1,4 @@ package dev.inmo.tgbotapi.bot.Ktor.base -import dev.inmo.micro_utils.coroutines.safely -import dev.inmo.tgbotapi.bot.Ktor.KtorCallFactory -import dev.inmo.tgbotapi.requests.DownloadFile -import dev.inmo.tgbotapi.requests.abstracts.Request -import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper -import io.ktor.client.HttpClient -import io.ktor.client.request.get -import kotlinx.serialization.json.Json - -object DownloadFileRequestCallFactory : KtorCallFactory { - override suspend fun makeCall( - client: HttpClient, - urlsKeeper: TelegramAPIUrlsKeeper, - request: Request, - jsonFormatter: Json - ): T? = (request as? DownloadFile) ?.let { - val fullUrl = urlsKeeper.createFileLinkUrl(it.filePath) - - safely { - @Suppress("UNCHECKED_CAST") - client.get(fullUrl) as T // always ByteArray - } - } -} +@Deprecated("Replaced", ReplaceWith("DownloadFileRequestCallFactory", "dev.inmo.tgbotapi.bot.ktor.base.DownloadFileRequestCallFactory")) +typealias DownloadFileRequestCallFactory = dev.inmo.tgbotapi.bot.ktor.base.DownloadFileRequestCallFactory 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 0a867ae31f..bc55fc3fd3 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 @@ -1,37 +1,4 @@ package dev.inmo.tgbotapi.bot.Ktor.base -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.http.Headers -import io.ktor.http.HttpHeaders - -class MultipartRequestCallFactory : AbstractRequestCallFactory() { - override fun prepareCallBody( - client: HttpClient, - urlsKeeper: TelegramAPIUrlsKeeper, - request: Request - ): Any? = (request as? MultipartRequest) ?.let { castedRequest -> - MultiPartFormDataContent( - formData { - val params = castedRequest.paramsJson.mapWithCommonValues() - for ((key, value) in castedRequest.mediaMap + params) { - when (value) { - is MultipartFile -> appendInput( - key, - Headers.build { - append(HttpHeaders.ContentDisposition, "filename=${value.filename}") - }, - block = value::input - ) - is FileId -> append(key, value.fileId) - else -> append(key, value.toString()) - } - } - } - ) - } -} +@Deprecated("Replaced", ReplaceWith("MultipartRequestCallFactory", "dev.inmo.tgbotapi.bot.ktor.base.MultipartRequestCallFactory")) +typealias MultipartRequestCallFactory = dev.inmo.tgbotapi.bot.ktor.base.MultipartRequestCallFactory diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/SimpleRequestCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/SimpleRequestCallFactory.kt index e774819815..56195671e8 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/SimpleRequestCallFactory.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/SimpleRequestCallFactory.kt @@ -1,23 +1,4 @@ package dev.inmo.tgbotapi.bot.Ktor.base -import dev.inmo.tgbotapi.bot.Ktor.KtorCallFactory -import dev.inmo.tgbotapi.requests.abstracts.* -import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper -import io.ktor.client.HttpClient -import io.ktor.http.ContentType -import io.ktor.http.content.TextContent - -class SimpleRequestCallFactory : AbstractRequestCallFactory() { - override fun prepareCallBody( - client: HttpClient, - urlsKeeper: TelegramAPIUrlsKeeper, - request: Request - ): Any? = (request as? SimpleRequest) ?.let { _ -> - val content = request.json().toString() - - TextContent( - content, - ContentType.Application.Json - ) - } -} +@Deprecated("Replaced", ReplaceWith("SimpleRequestCallFactory", "dev.inmo.tgbotapi.bot.ktor.base.SimpleRequestCallFactory")) +typealias SimpleRequestCallFactory = dev.inmo.tgbotapi.bot.ktor.base.SimpleRequestCallFactory diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorCallFactory.kt new file mode 100644 index 0000000000..5df7001649 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorCallFactory.kt @@ -0,0 +1,15 @@ +package dev.inmo.tgbotapi.bot.ktor + +import dev.inmo.tgbotapi.requests.abstracts.Request +import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper +import io.ktor.client.HttpClient +import kotlinx.serialization.json.Json + +interface KtorCallFactory { + suspend fun makeCall( + client: HttpClient, + urlsKeeper: TelegramAPIUrlsKeeper, + request: Request, + jsonFormatter: Json + ): T? +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorPipelineStepsHolder.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorPipelineStepsHolder.kt index 725a2a8bc6..84fd53fca8 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorPipelineStepsHolder.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorPipelineStepsHolder.kt @@ -1,6 +1,5 @@ package dev.inmo.tgbotapi.bot.ktor -import dev.inmo.tgbotapi.bot.Ktor.KtorCallFactory import dev.inmo.tgbotapi.requests.abstracts.Request interface KtorPipelineStepsHolder { 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 new file mode 100644 index 0000000000..2d76e55982 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt @@ -0,0 +1,128 @@ +package dev.inmo.tgbotapi.bot.ktor + +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.newRequestException +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.features.* +import io.ktor.client.statement.readText +import kotlinx.serialization.json.Json + +@RiskFeature +fun createTelegramBotDefaultKtorCallRequestsFactories() = listOf( + SimpleRequestCallFactory(), + MultipartRequestCallFactory(), + DownloadFileRequestCallFactory, + DownloadFileChannelRequestCallFactory +) + +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.feature(HttpTimeout) == null) { + install(HttpTimeout) + } + } + + override suspend fun execute(request: Request): T { + return runCatching { + safely( + { e -> + pipelineStepsHolder.onRequestException(request, e) ?.let { return@safely it } + + throw if (e is ClientRequestException) { + val content = e.response.readText() + val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) + newRequestException( + responseObject, + content, + "Can't get result object from $content" + ) + } else { + e + } + + } + ) { + pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories) + requestsLimiter.limit { + 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 { + pipelineStepsHolder.onRequestReturnResult(it, 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, + builder: KtorRequestsExecutorBuilder.() -> Unit = {} +): TelegramBot = telegramBot(TelegramAPIUrlsKeeper(token, 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 new file mode 100644 index 0000000000..5078042d5a --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/AbstractRequestCallFactory.kt @@ -0,0 +1,79 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +import dev.inmo.micro_utils.coroutines.safelyWithResult +import dev.inmo.tgbotapi.bot.ktor.KtorCallFactory +import dev.inmo.tgbotapi.bot.exceptions.newRequestException +import dev.inmo.tgbotapi.requests.GetUpdates +import dev.inmo.tgbotapi.requests.abstracts.Request +import dev.inmo.tgbotapi.types.Response +import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper +import io.ktor.client.HttpClient +import io.ktor.client.call.receive +import io.ktor.client.features.timeout +import io.ktor.client.request.* +import io.ktor.client.statement.HttpResponse +import io.ktor.http.ContentType +import kotlinx.serialization.json.Json +import kotlin.collections.set + +var defaultUpdateTimeoutForZeroDelay = 1000L + +abstract class AbstractRequestCallFactory : KtorCallFactory { + private val methodsCache: MutableMap = mutableMapOf() + override suspend fun makeCall( + client: HttpClient, + urlsKeeper: TelegramAPIUrlsKeeper, + request: Request, + jsonFormatter: Json + ): T? { + val preparedBody = prepareCallBody(client, urlsKeeper, request) ?: return null + + client.post { + url( + methodsCache[request.method()] ?: "${urlsKeeper.commonAPIUrl}/${request.method()}".also { + methodsCache[request.method()] = it + } + ) + accept(ContentType.Application.Json) + + if (request is GetUpdates) { + request.timeout?.times(1000L)?.let { customTimeoutMillis -> + if (customTimeoutMillis > 0) { + timeout { + requestTimeoutMillis = customTimeoutMillis + socketTimeoutMillis = customTimeoutMillis + } + } else { + timeout { + requestTimeoutMillis = defaultUpdateTimeoutForZeroDelay + socketTimeoutMillis = defaultUpdateTimeoutForZeroDelay + } + } + } + } + + body = preparedBody + }.let { response -> + val content = response.receive() + val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) + + return safelyWithResult { + (responseObject.result?.let { + jsonFormatter.decodeFromJsonElement(request.resultDeserializer, it) + } ?: response.let { + throw newRequestException( + responseObject, + content, + "Can't get result object from $content" + ) + }) + }.getOrThrow() + } + } + + protected abstract fun prepareCallBody( + client: HttpClient, + urlsKeeper: TelegramAPIUrlsKeeper, + request: Request + ): Any? +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/DownloadFileChannelRequestCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/DownloadFileChannelRequestCallFactory.kt new file mode 100644 index 0000000000..083dcb571b --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/DownloadFileChannelRequestCallFactory.kt @@ -0,0 +1,41 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +import dev.inmo.micro_utils.coroutines.* +import dev.inmo.tgbotapi.bot.ktor.KtorCallFactory +import dev.inmo.tgbotapi.requests.DownloadFileStream +import dev.inmo.tgbotapi.requests.abstracts.Request +import dev.inmo.tgbotapi.utils.ByteReadChannelAllocator +import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper +import io.ktor.client.HttpClient +import io.ktor.client.call.receive +import io.ktor.client.request.get +import io.ktor.client.statement.HttpStatement +import io.ktor.utils.io.* +import kotlinx.coroutines.* +import kotlinx.serialization.json.Json + +object DownloadFileChannelRequestCallFactory : KtorCallFactory { + override suspend fun makeCall( + client: HttpClient, + urlsKeeper: TelegramAPIUrlsKeeper, + request: Request, + jsonFormatter: Json + ): T? = (request as? DownloadFileStream) ?.let { + val fullUrl = urlsKeeper.createFileLinkUrl(it.filePath) + + ByteReadChannelAllocator { + val scope = CoroutineScope(currentCoroutineContext() + SupervisorJob()) + val outChannel = ByteChannel() + scope.launch { + runCatchingSafely { + client.get(fullUrl).execute { httpResponse -> + val channel: ByteReadChannel = httpResponse.receive() + channel.copyAndClose(outChannel) + } + } + scope.cancel() + } + outChannel + } as T + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/DownloadFileRequestCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/DownloadFileRequestCallFactory.kt new file mode 100644 index 0000000000..4edcfda6c8 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/DownloadFileRequestCallFactory.kt @@ -0,0 +1,26 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +import dev.inmo.micro_utils.coroutines.safely +import dev.inmo.tgbotapi.bot.ktor.KtorCallFactory +import dev.inmo.tgbotapi.requests.DownloadFile +import dev.inmo.tgbotapi.requests.abstracts.Request +import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import kotlinx.serialization.json.Json + +object DownloadFileRequestCallFactory : KtorCallFactory { + override suspend fun makeCall( + client: HttpClient, + urlsKeeper: TelegramAPIUrlsKeeper, + request: Request, + jsonFormatter: Json + ): T? = (request as? DownloadFile) ?.let { + val fullUrl = urlsKeeper.createFileLinkUrl(it.filePath) + + safely { + @Suppress("UNCHECKED_CAST") + client.get(fullUrl) as T // always ByteArray + } + } +} 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 new file mode 100644 index 0000000000..f52a9e87b3 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/MultipartRequestCallFactory.kt @@ -0,0 +1,37 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +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.http.Headers +import io.ktor.http.HttpHeaders + +class MultipartRequestCallFactory : AbstractRequestCallFactory() { + override fun prepareCallBody( + client: HttpClient, + urlsKeeper: TelegramAPIUrlsKeeper, + request: Request + ): Any? = (request as? MultipartRequest) ?.let { castedRequest -> + MultiPartFormDataContent( + formData { + val params = castedRequest.paramsJson.mapWithCommonValues() + for ((key, value) in castedRequest.mediaMap + params) { + when (value) { + is MultipartFile -> appendInput( + key, + Headers.build { + append(HttpHeaders.ContentDisposition, "filename=${value.filename}") + }, + block = value::input + ) + is FileId -> append(key, value.fileId) + else -> append(key, value.toString()) + } + } + } + ) + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/SimpleRequestCallFactory.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/SimpleRequestCallFactory.kt new file mode 100644 index 0000000000..686d1df365 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/base/SimpleRequestCallFactory.kt @@ -0,0 +1,22 @@ +package dev.inmo.tgbotapi.bot.ktor.base + +import dev.inmo.tgbotapi.requests.abstracts.* +import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper +import io.ktor.client.HttpClient +import io.ktor.http.ContentType +import io.ktor.http.content.TextContent + +class SimpleRequestCallFactory : AbstractRequestCallFactory() { + override fun prepareCallBody( + client: HttpClient, + urlsKeeper: TelegramAPIUrlsKeeper, + request: Request + ): Any? = (request as? SimpleRequest) ?.let { _ -> + val content = request.json().toString() + + TextContent( + content, + ContentType.Application.Json + ) + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/multiserver/SimpleMultiServerRequestsExecutor.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/multiserver/SimpleMultiServerRequestsExecutor.kt index 1dd4e28076..bed97ffbed 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/multiserver/SimpleMultiServerRequestsExecutor.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/multiserver/SimpleMultiServerRequestsExecutor.kt @@ -1,12 +1,11 @@ package dev.inmo.tgbotapi.bot.multiserver -import dev.inmo.tgbotapi.bot.Ktor.KtorRequestsExecutorBuilder -import dev.inmo.tgbotapi.bot.Ktor.telegramBot +import dev.inmo.tgbotapi.bot.ktor.KtorRequestsExecutorBuilder +import dev.inmo.tgbotapi.bot.ktor.telegramBot import dev.inmo.tgbotapi.bot.RequestsExecutor import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.requests.abstracts.Request import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper -import dev.inmo.tgbotapi.utils.telegramBotAPIDefaultUrl import kotlinx.coroutines.* import kotlin.js.JsName import kotlin.jvm.JvmName