diff --git a/cache/media/build.gradle b/cache/content/common/build.gradle similarity index 52% rename from cache/media/build.gradle rename to cache/content/common/build.gradle index 8a22926..1c05fde 100644 --- a/cache/media/build.gradle +++ b/cache/content/common/build.gradle @@ -6,3 +6,13 @@ plugins { apply from: "$mppProjectWithSerializationPresetPath" +kotlin { + sourceSets { + commonMain { + dependencies { + api "dev.inmo:tgbotapi.core:$tgbotapi_version" + } + } + } +} + diff --git a/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/DefaultMessageContentCache.kt b/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/DefaultMessageContentCache.kt new file mode 100644 index 0000000..755e430 --- /dev/null +++ b/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/DefaultMessageContentCache.kt @@ -0,0 +1,128 @@ +package dev.inmo.tgbotapi.libraries.cache.media.common + +import dev.inmo.tgbotapi.bot.TelegramBot +import dev.inmo.tgbotapi.requests.DownloadFileStream +import dev.inmo.tgbotapi.requests.abstracts.MultipartFile +import dev.inmo.tgbotapi.requests.get.GetFile +import dev.inmo.tgbotapi.requests.send.media.* +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.InputMedia.* +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaContent +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent +import dev.inmo.tgbotapi.utils.asInput +import io.ktor.utils.io.core.Input + +class DefaultMessageContentCache( + private val bot: TelegramBot, + private val filesRefreshingChatId: ChatId, + private val simpleMessageContentCache: MessagesSimpleCache, + private val messagesFilesCache: MessagesFilesCache = InMemoryMessagesFilesCache() +) : MessageContentCache { + override suspend fun save(content: MessageContent): K { + return when (content) { + is MediaContent -> { + val extendedInfo = bot.execute( + GetFile(content.media.fileId) + ) + val allocator = bot.execute( + DownloadFileStream( + extendedInfo.filePath + ) + ) + + save(content, extendedInfo.fileName) { + allocator.invoke().asInput() + } + } + else -> simpleMessageContentCache.add(content) + } + } + + override suspend fun save( + content: MediaContent, + filename: String, + inputAllocator: suspend () -> Input + ): K { + val key = simpleMessageContentCache.add(content) + runCatching { + messagesFilesCache.set(key, filename, inputAllocator) + }.onFailure { + simpleMessageContentCache.remove(key) + } + + return key + } + + override suspend fun get(k: K): MessageContent? { + val savedSimpleContent = simpleMessageContentCache.get(k) ?: return null + + if (savedSimpleContent is MediaContent) { + runCatching { + bot.execute(GetFile(savedSimpleContent.media.fileId)) + }.onFailure { + val savedFileContentAllocator = messagesFilesCache.get(k) ?: error("Unexpected absence of $k file for content ($simpleMessageContentCache)") + val newContent = bot.execute( + when (savedSimpleContent.asInputMedia()) { + is InputMediaAnimation -> SendAnimation( + filesRefreshingChatId, + MultipartFile( + savedFileContentAllocator + ), + disableNotification = true + ) + is InputMediaAudio -> SendAudio( + filesRefreshingChatId, + MultipartFile( + savedFileContentAllocator + ), + disableNotification = true + ) + is InputMediaVideo -> SendVideo( + filesRefreshingChatId, + MultipartFile( + savedFileContentAllocator + ), + disableNotification = true + ) + is InputMediaDocument -> SendDocument( + filesRefreshingChatId, + MultipartFile( + savedFileContentAllocator + ), + disableNotification = true + ) + is InputMediaPhoto -> SendPhoto( + filesRefreshingChatId, + MultipartFile( + savedFileContentAllocator + ), + disableNotification = true + ) + } + ) + + simpleMessageContentCache.update(k, newContent.content) + return newContent.content + } + } + return savedSimpleContent + } + + override suspend fun contains(k: K): Boolean { + return simpleMessageContentCache.contains(k) + } + + override suspend fun remove(k: K) { + simpleMessageContentCache.remove(k) + messagesFilesCache.remove(k) + } + + companion object { + operator fun invoke( + bot: TelegramBot, + filesRefreshingChatId: ChatId, + simpleMessageContentCache: MessagesSimpleCache = InMemoryMessagesSimpleCache(), + messagesFilesCache: MessagesFilesCache = InMemoryMessagesFilesCache() + ) = DefaultMessageContentCache(bot, filesRefreshingChatId, simpleMessageContentCache, messagesFilesCache) + } +} diff --git a/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/MessageContentCache.kt b/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/MessageContentCache.kt new file mode 100644 index 0000000..baec180 --- /dev/null +++ b/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/MessageContentCache.kt @@ -0,0 +1,17 @@ +package dev.inmo.tgbotapi.libraries.cache.media.common + +import dev.inmo.tgbotapi.types.message.content.abstracts.MediaContent +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent +import io.ktor.utils.io.core.Input + +interface MessageContentCache { + suspend fun save(content: MessageContent): K + suspend fun save( + content: MediaContent, + filename: String, + inputAllocator: suspend () -> Input + ): K + suspend fun get(k: K): MessageContent? + suspend fun contains(k: K): Boolean + suspend fun remove(k: K) +} diff --git a/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/MessagesFilesCache.kt b/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/MessagesFilesCache.kt new file mode 100644 index 0000000..796fca9 --- /dev/null +++ b/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/MessagesFilesCache.kt @@ -0,0 +1,41 @@ +package dev.inmo.tgbotapi.libraries.cache.media.common + +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.MessageIdentifier +import dev.inmo.tgbotapi.utils.StorageFile +import io.ktor.utils.io.core.* + +interface MessagesFilesCache { + suspend fun set(k: K, filename: String, inputAllocator: suspend () -> Input) + suspend fun get(k: K): StorageFile? + suspend fun remove(k: K) + suspend fun contains(k: K): Boolean +} + +/** + * It is not recommended to use in production realization of [MessagesFilesCache] which has been created for fast + * start of application creation with usage of [MessageContentCache] with aim to replace this realization by some + * disks-oriented one + */ +class InMemoryMessagesFilesCache : MessagesFilesCache { + private val map = mutableMapOf() + + override suspend fun set(k: K, filename: String, inputAllocator: suspend () -> Input) { + map[k] = StorageFile( + filename, + inputAllocator().readBytes() + ) + } + + override suspend fun get(k: K): StorageFile? { + return map[k] + } + + override suspend fun remove(k: K) { + map.remove(k) + } + + override suspend fun contains(k: K): Boolean { + return map.contains(k) + } +} diff --git a/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/MessagesSimpleCache.kt b/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/MessagesSimpleCache.kt new file mode 100644 index 0000000..ffc08b1 --- /dev/null +++ b/cache/content/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/MessagesSimpleCache.kt @@ -0,0 +1,63 @@ +package dev.inmo.tgbotapi.libraries.cache.media.common + +import com.benasher44.uuid.uuid4 +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent + +interface MessagesSimpleCache { + suspend fun add(content: MessageContent): K + suspend fun update(k: K, content: MessageContent): Boolean + suspend fun get(k: K): MessageContent? + suspend fun remove(k: K) + suspend fun contains(k: K): Boolean +} + +/** + * It is not recommended to use in production realization of [MessagesFilesCache] which has been created for fast + * start of application creation with usage of [MessageContentCache] with aim to replace this realization by some + * disks-oriented one + */ +class InMemoryMessagesSimpleCache( + private val keyGenerator: () -> K +) : MessagesSimpleCache { + private val map = mutableMapOf() + + override suspend fun add( + content: MessageContent + ): K { + val key = keyGenerator() + map[key] = content + return key + } + + override suspend fun update( + k: K, + content: MessageContent + ): Boolean { + return map.runCatching { + if (contains(k)) { + put(k, content) + true + } else { + false + } + }.getOrDefault(false) + } + + override suspend fun get(k: K): MessageContent? { + return map[k] + } + + override suspend fun remove(k: K) { + map.remove(k) + } + + override suspend fun contains(k: K): Boolean { + return map.contains(k) + } + + companion object { + operator fun invoke() = InMemoryMessagesSimpleCache { + uuid4().toString() + } + } +} diff --git a/cache/content/common/src/jvmMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/InFilesMessagesFilesCache.kt b/cache/content/common/src/jvmMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/InFilesMessagesFilesCache.kt new file mode 100644 index 0000000..53aed1c --- /dev/null +++ b/cache/content/common/src/jvmMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/common/InFilesMessagesFilesCache.kt @@ -0,0 +1,74 @@ +package dev.inmo.tgbotapi.libraries.cache.media.common + +import dev.inmo.tgbotapi.utils.* +import io.ktor.utils.io.core.Input +import io.ktor.utils.io.core.copyTo +import io.ktor.utils.io.streams.asInput +import io.ktor.utils.io.streams.asOutput +import java.io.File + +class InFilesMessagesFilesCache( + private val folderFile: File, + private val filePrefixBuilder: (K) -> String +) : MessagesFilesCache { + private val K.storageFile: StorageFile? + get() { + val prefix = filePrefix(this) + val filename = folderFile.list() ?.firstOrNull { it.startsWith(prefix) } ?: return null + val file = File(folderFile, filename) + val storageFileFilename = file.name.removePrefix("$prefix ") + + return StorageFile( + StorageFileInfo(storageFileFilename) + ) { + file.inputStream().asInput() + } + } + + init { + require(!folderFile.isFile) { "Folder of messages files cache can't be file, but was $folderFile" } + folderFile.mkdirs() + } + + private fun filePrefix(k: K): String = filePrefixBuilder(k) + + private fun fileName(k: K, filename: String): String { + return "${filePrefix(k)} $filename" + } + + override suspend fun set(k: K, filename: String, inputAllocator: suspend () -> Input) { + val fullFileName = fileName(k, filename) + val file = File(folderFile, fullFileName).apply { + delete() + } + inputAllocator().use { input -> + file.outputStream().asOutput().use { output -> + input.copyTo(output) + } + } + } + + override suspend fun get(k: K): StorageFile? { + return k.storageFile + } + + override suspend fun remove(k: K) { + val prefix = filePrefix(k) + folderFile.listFiles() ?.forEach { + if (it.name.startsWith(prefix)) { + it.delete() + } + } + } + + override suspend fun contains(k: K): Boolean { + val prefix = filePrefix(k) + return folderFile.list() ?.any { it.startsWith(prefix) } == true + } + + companion object { + operator fun invoke(folderFile: File) = InFilesMessagesFilesCache( + folderFile + ) { it } + } +} diff --git a/cache/content/common/src/main/AndroidManifest.xml b/cache/content/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e663aab --- /dev/null +++ b/cache/content/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/cache/content/micro_utils/build.gradle b/cache/content/micro_utils/build.gradle new file mode 100644 index 0000000..0dcdc86 --- /dev/null +++ b/cache/content/micro_utils/build.gradle @@ -0,0 +1,19 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api "dev.inmo:micro_utils.repos.common:$micro_utils_version" + api project(":tgbotapi.libraries.cache.content.common") + } + } + } +} + diff --git a/cache/content/micro_utils/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/micro_utils/SimpleKeyValueMessageContentCache.kt b/cache/content/micro_utils/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/micro_utils/SimpleKeyValueMessageContentCache.kt new file mode 100644 index 0000000..e77a7db --- /dev/null +++ b/cache/content/micro_utils/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/media/micro_utils/SimpleKeyValueMessageContentCache.kt @@ -0,0 +1,104 @@ +package dev.inmo.tgbotapi.libraries.cache.media.micro_utils + +import com.benasher44.uuid.uuid4 +import dev.inmo.micro_utils.repos.* +import dev.inmo.micro_utils.repos.mappers.withMapper +import dev.inmo.tgbotapi.libraries.cache.media.common.MessagesSimpleCache +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.MessageIdentifier +import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent +import kotlinx.serialization.* +import kotlinx.serialization.builtins.PairSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlin.js.JsName +import kotlin.jvm.JvmName + +class SimpleKeyValueMessageContentCache( + private val keyValueRepo: KeyValueRepo, + private val keyGenerator: () -> K +) : MessagesSimpleCache { + override suspend fun add(content: MessageContent): K { + val key = keyGenerator() + keyValueRepo.set(key, content) + + return key + } + + override suspend fun update(k: K, content: MessageContent): Boolean { + return keyValueRepo.runCatching { + if (contains(k)) { + keyValueRepo.set(k, content) + true + } else { + false + } + }.getOrDefault(false) + } + + override suspend fun get(k: K): MessageContent? { + return keyValueRepo.get(k) + } + + override suspend fun contains(k: K): Boolean { + return keyValueRepo.contains(k) + } + + override suspend fun remove(k: K) { + keyValueRepo.unset(k) + } + + companion object { + operator fun invoke( + keyValueRepo: KeyValueRepo + ) = SimpleKeyValueMessageContentCache(keyValueRepo) { uuid4().toString() } + } +} + +val chatIdToMessageIdentifierSerializer = PairSerializer( + ChatId.serializer(), + MessageIdentifier.serializer() +) + +val messageContentSerializer = PolymorphicSerializer(MessageContent::class) + +inline fun KeyValueRepo.asMessageContentCache( + serialFormatCreator: (SerializersModule) -> StringFormat = { Json { serializersModule = it } } +): StandardKeyValueRepo, MessageContent> { + val serialFormat = serialFormatCreator(MessageContent.serializationModule()) + return withMapper, MessageContent, String, String>( + { serialFormat.encodeToString(chatIdToMessageIdentifierSerializer, this) }, + { serialFormat.encodeToString(messageContentSerializer, this) }, + { serialFormat.decodeFromString(chatIdToMessageIdentifierSerializer, this) }, + { serialFormat.decodeFromString(messageContentSerializer, this) }, + ) +} + +@JvmName("stringsKeyValueAsHexMessageContentCache") +@JsName("stringsKeyValueAsHexMessageContentCache") +inline fun KeyValueRepo.asMessageContentCache( + serialFormatCreator: (SerializersModule) -> BinaryFormat +): StandardKeyValueRepo, MessageContent> { + val serialFormat = serialFormatCreator(MessageContent.serializationModule()) + return withMapper, MessageContent, String, String>( + { serialFormat.encodeToHexString(chatIdToMessageIdentifierSerializer, this) }, + { serialFormat.encodeToHexString(messageContentSerializer, this) }, + { serialFormat.decodeFromHexString(chatIdToMessageIdentifierSerializer, this) }, + { serialFormat.decodeFromHexString(messageContentSerializer, this) }, + ) +} + +@JvmName("bytesKeyValueAsMessageContentCache") +@JsName("bytesKeyValueAsMessageContentCache") +inline fun KeyValueRepo.asMessageContentCache( + serialFormatCreator: (SerializersModule) -> BinaryFormat +): StandardKeyValueRepo, MessageContent> { + val serialFormat = serialFormatCreator(MessageContent.serializationModule()) + return withMapper, MessageContent, ByteArray, ByteArray>( + { serialFormat.encodeToByteArray(chatIdToMessageIdentifierSerializer, this) }, + { serialFormat.encodeToByteArray(messageContentSerializer, this) }, + { serialFormat.decodeFromByteArray(chatIdToMessageIdentifierSerializer, this) }, + { serialFormat.decodeFromByteArray(messageContentSerializer, this) }, + ) +} diff --git a/cache/content/micro_utils/src/main/AndroidManifest.xml b/cache/content/micro_utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000..77c705e --- /dev/null +++ b/cache/content/micro_utils/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/cache/media/src/main/AndroidManifest.xml b/cache/media/src/main/AndroidManifest.xml deleted file mode 100644 index 24d7792..0000000 --- a/cache/media/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/gradle.properties b/gradle.properties index 72640f8..260a53d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,9 +11,9 @@ kotlin_serialisation_core_version=1.3.2 github_release_plugin_version=2.2.12 -tgbotapi_version=0.38.4 -micro_utils_version=0.9.5 -exposed_version=0.37.2 +tgbotapi_version=0.38.12 +micro_utils_version=0.9.20 +exposed_version=0.37.3 plagubot_version=0.5.1 # ANDROID @@ -33,5 +33,5 @@ dokka_version=1.6.10 # Project data group=dev.inmo -version=0.0.16 -android_code_version=16 +version=0.0.17 +android_code_version=17 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102..00e33ed 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index 40732cb..ccfc849 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,13 +4,15 @@ String[] includes = [ ":cache:admins:common", ":cache:admins:micro_utils", ":cache:admins:plagubot", - ":cache:media" + + ":cache:content:common", + ":cache:content:micro_utils", ] includes.each { originalName -> - String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replaceAll(":", File.separator)}" - String projectName = "${rootProject.name}${originalName.replaceAll(":", ".")}" + String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replace(":", File.separator)}" + String projectName = "${rootProject.name}${originalName.replace(":", ".")}" String projectIdentifier = ":${projectName}" include projectIdentifier ProjectDescriptor project = project(projectIdentifier)