From b475976ff10241862b434c3540f809e1105acf85 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Mon, 9 Aug 2021 23:22:08 +0600 Subject: [PATCH] add streaming file downloading feature --- .../tgbotapi/bot/Ktor/KtorRequestsExecutor.kt | 10 ++++- .../DownloadFileChannelRequestCallFactory.kt | 38 +++++++++++++++++++ .../base/DownloadFileRequestCallFactory.kt | 2 +- .../tgbotapi/requests/DownloadFileStream.kt | 15 ++++++++ .../tgbotapi/utils/InputStreamAllocator.kt | 31 +++++++++++++++ .../tgbotapi/utils/TelegramAPIUrlsKeeper.kt | 2 + 6 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/DownloadFileChannelRequestCallFactory.kt create mode 100644 tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/DownloadFileStream.kt create mode 100644 tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/InputStreamAllocator.kt 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 65a0f62e2f..ebbc38bebb 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 @@ -42,6 +42,14 @@ inline fun telegramBot( crossinline builder: KtorRequestsExecutorBuilder.() -> Unit = {} ): TelegramBot = telegramBot(TelegramAPIUrlsKeeper(token, apiUrl), builder) +@RiskFeature +fun createTelegramBotDefaultKtorCallRequestsFactories() = listOf( + SimpleRequestCallFactory(), + MultipartRequestCallFactory(), + DownloadFileRequestCallFactory, + DownloadFileChannelRequestCallFactory +) + class KtorRequestsExecutor( telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper, client: HttpClient = HttpClient(), @@ -52,7 +60,7 @@ class KtorRequestsExecutor( ) : BaseRequestsExecutor(telegramAPIUrlsKeeper) { private val callsFactories: List = callsFactories.run { if (!excludeDefaultFactories) { - this + listOf(SimpleRequestCallFactory(), MultipartRequestCallFactory(), DownloadFileRequestCallFactory) + this + createTelegramBotDefaultKtorCallRequestsFactories() } else { this } 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..fa8526d5c1 --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/Ktor/base/DownloadFileChannelRequestCallFactory.kt @@ -0,0 +1,38 @@ +package dev.inmo.tgbotapi.bot.Ktor.base + +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.InputStreamAllocator +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) + + return InputStreamAllocator { + val scope = CoroutineScope(coroutineContext) + val outChannel = ByteChannel() + scope.launch { + client.get(fullUrl).execute { httpResponse -> + val channel: ByteReadChannel = httpResponse.receive() + channel.copyTo(outChannel) + } + } + 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 index cbccc36e5c..aec803e5f8 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 @@ -16,7 +16,7 @@ object DownloadFileRequestCallFactory : KtorCallFactory { request: Request, jsonFormatter: Json ): T? = (request as? DownloadFile) ?.let { - val fullUrl = "${urlsKeeper.fileBaseUrl}/${it.filePath}" + val fullUrl = urlsKeeper.createFileLinkUrl(it.filePath) return safely { @Suppress("UNCHECKED_CAST") diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/DownloadFileStream.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/DownloadFileStream.kt new file mode 100644 index 0000000000..a1be40d3bf --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/DownloadFileStream.kt @@ -0,0 +1,15 @@ +package dev.inmo.tgbotapi.requests + +import dev.inmo.tgbotapi.requests.abstracts.Request +import dev.inmo.tgbotapi.utils.InputStreamAllocator +import dev.inmo.tgbotapi.utils.InputStreamAllocatorSerializer +import kotlinx.serialization.DeserializationStrategy + +class DownloadFileStream( + val filePath: String +) : Request { + override fun method(): String = filePath + override val resultDeserializer: DeserializationStrategy + get() = InputStreamAllocatorSerializer + +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/InputStreamAllocator.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/InputStreamAllocator.kt new file mode 100644 index 0000000000..6effad4d0c --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/InputStreamAllocator.kt @@ -0,0 +1,31 @@ +package dev.inmo.tgbotapi.utils + +import dev.inmo.micro_utils.common.ByteArrayAllocatorSerializer +import io.ktor.util.toByteArray +import io.ktor.utils.io.ByteReadChannel +import kotlinx.coroutines.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +fun interface InputStreamAllocator { + suspend operator fun invoke(): ByteReadChannel +} + +object InputStreamAllocatorSerializer : KSerializer { + override val descriptor: SerialDescriptor = ByteArrayAllocatorSerializer.descriptor + + override fun serialize(encoder: Encoder, value: InputStreamAllocator) { + TODO("Not yet implemented") +// ByteArrayAllocatorSerializer.serialize( +// encoder +// ) { +// } + } + + override fun deserialize(decoder: Decoder): InputStreamAllocator { + val byteArrayAllocator = ByteArrayAllocatorSerializer.deserialize(decoder) + return InputStreamAllocator { ByteReadChannel(byteArrayAllocator()) } + } +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/TelegramAPIUrlsKeeper.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/TelegramAPIUrlsKeeper.kt index 100781d435..422634479e 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/TelegramAPIUrlsKeeper.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/utils/TelegramAPIUrlsKeeper.kt @@ -26,4 +26,6 @@ class TelegramAPIUrlsKeeper( commonAPIUrl = "$correctedHost/bot$token" fileBaseUrl = "$correctedHost/file/bot$token" } + + fun createFileLinkUrl(filePath: String) = "${fileBaseUrl}/$filePath" }