mirror of
https://github.com/InsanusMokrassar/TelegramBotApiLibraries.git
synced 2024-11-17 05:43:51 +00:00
implement content caching
This commit is contained in:
parent
6591c8ffa8
commit
f423d31423
@ -6,3 +6,13 @@ plugins {
|
||||
|
||||
apply from: "$mppProjectWithSerializationPresetPath"
|
||||
|
||||
kotlin {
|
||||
sourceSets {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api "dev.inmo:tgbotapi.core:$tgbotapi_version"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,119 @@
|
||||
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.MessageIdentifier
|
||||
import dev.inmo.tgbotapi.types.message.content.abstracts.MediaContent
|
||||
import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent
|
||||
import io.ktor.utils.io.cancel
|
||||
|
||||
class DefaultMessageContentCache<T>(
|
||||
private val bot: TelegramBot,
|
||||
private val simpleMessageContentCache: MessageContentCache,
|
||||
private val messagesFilesCache: MessagesFilesCache,
|
||||
private val filesRefreshingChatId: ChatId
|
||||
) : MessageContentCache {
|
||||
override suspend fun save(chatId: ChatId, messageId: MessageIdentifier, content: MessageContent): Boolean {
|
||||
runCatching {
|
||||
if (content is MediaContent) {
|
||||
val extendedInfo = bot.execute(
|
||||
GetFile(content.media.fileId)
|
||||
)
|
||||
val allocator = bot.execute(
|
||||
DownloadFileStream(
|
||||
extendedInfo.filePath
|
||||
)
|
||||
)
|
||||
messagesFilesCache.set(chatId, messageId, extendedInfo.fileName, allocator)
|
||||
}
|
||||
}.onFailure {
|
||||
return false
|
||||
}
|
||||
|
||||
return simpleMessageContentCache.save(
|
||||
chatId, messageId, content
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun get(chatId: ChatId, messageId: MessageIdentifier): MessageContent? {
|
||||
val savedSimpleContent = simpleMessageContentCache.get(chatId, messageId) ?: return null
|
||||
|
||||
if (savedSimpleContent is MediaContent) {
|
||||
runCatching {
|
||||
val streamAllocator = bot.execute(
|
||||
DownloadFileStream(
|
||||
bot.execute(
|
||||
GetFile(
|
||||
savedSimpleContent.media.fileId
|
||||
)
|
||||
).filePath
|
||||
)
|
||||
)
|
||||
|
||||
streamAllocator().apply {
|
||||
readByte()
|
||||
cancel()
|
||||
}
|
||||
}.onFailure {
|
||||
val savedFileContentAllocator = messagesFilesCache.get(chatId, messageId) ?: error("Unexpected absence of $chatId:$messageId 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.save(chatId, messageId, newContent.content)
|
||||
return newContent.content
|
||||
}
|
||||
}
|
||||
return savedSimpleContent
|
||||
}
|
||||
|
||||
override suspend fun contains(chatId: ChatId, messageId: MessageIdentifier): Boolean {
|
||||
return simpleMessageContentCache.contains(chatId, messageId)
|
||||
}
|
||||
|
||||
override suspend fun remove(chatId: ChatId, messageId: MessageIdentifier) {
|
||||
simpleMessageContentCache.remove(chatId, messageId)
|
||||
messagesFilesCache.remove(chatId, messageId)
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package dev.inmo.tgbotapi.libraries.cache.media.common
|
||||
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.MessageIdentifier
|
||||
import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent
|
||||
|
||||
interface MessageContentCache {
|
||||
suspend fun save(chatId: ChatId, messageId: MessageIdentifier, content: MessageContent): Boolean
|
||||
suspend fun get(chatId: ChatId, messageId: MessageIdentifier): MessageContent?
|
||||
suspend fun contains(chatId: ChatId, messageId: MessageIdentifier): Boolean
|
||||
suspend fun remove(chatId: ChatId, messageId: MessageIdentifier)
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
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.ByteReadChannelAllocator
|
||||
import dev.inmo.tgbotapi.utils.StorageFile
|
||||
import io.ktor.util.toByteArray
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
|
||||
interface MessagesFilesCache {
|
||||
suspend fun set(chatId: ChatId, messageIdentifier: MessageIdentifier, filename: String, byteReadChannelAllocator: ByteReadChannelAllocator)
|
||||
suspend fun get(chatId: ChatId, messageIdentifier: MessageIdentifier): StorageFile?
|
||||
suspend fun remove(chatId: ChatId, messageIdentifier: MessageIdentifier)
|
||||
suspend fun contains(chatId: ChatId, messageIdentifier: MessageIdentifier): 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<Pair<ChatId, MessageIdentifier>, StorageFile>()
|
||||
|
||||
override suspend fun set(
|
||||
chatId: ChatId,
|
||||
messageIdentifier: MessageIdentifier,
|
||||
filename: String,
|
||||
byteReadChannelAllocator: ByteReadChannelAllocator
|
||||
) {
|
||||
map[chatId to messageIdentifier] = StorageFile(
|
||||
filename,
|
||||
byteReadChannelAllocator.invoke().toByteArray()
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun get(chatId: ChatId, messageIdentifier: MessageIdentifier): StorageFile? {
|
||||
return map[chatId to messageIdentifier]
|
||||
}
|
||||
|
||||
override suspend fun remove(chatId: ChatId, messageIdentifier: MessageIdentifier) {
|
||||
map.remove(chatId to messageIdentifier)
|
||||
}
|
||||
|
||||
override suspend fun contains(chatId: ChatId, messageIdentifier: MessageIdentifier): Boolean {
|
||||
return map.contains(chatId to messageIdentifier)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
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.*
|
||||
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
|
||||
) : MessagesFilesCache {
|
||||
private val Pair<ChatId, MessageIdentifier>.storageFile: StorageFile?
|
||||
get() {
|
||||
val prefix = filePrefix(first, second)
|
||||
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(chatId: ChatId, messageIdentifier: MessageIdentifier): String {
|
||||
return "${chatId.chatId} $messageIdentifier"
|
||||
}
|
||||
|
||||
private fun fileName(chatId: ChatId, messageIdentifier: MessageIdentifier, filename: String): String {
|
||||
return "${chatId.chatId} $messageIdentifier $filename"
|
||||
}
|
||||
|
||||
override suspend fun set(
|
||||
chatId: ChatId,
|
||||
messageIdentifier: MessageIdentifier,
|
||||
filename: String,
|
||||
byteReadChannelAllocator: ByteReadChannelAllocator
|
||||
) {
|
||||
val fullFileName = fileName(chatId, messageIdentifier, filename)
|
||||
val file = File(folderFile, fullFileName).apply {
|
||||
delete()
|
||||
}
|
||||
byteReadChannelAllocator.invoke().asInput().use { input ->
|
||||
file.outputStream().asOutput().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun get(chatId: ChatId, messageIdentifier: MessageIdentifier): StorageFile? {
|
||||
return (chatId to messageIdentifier).storageFile
|
||||
}
|
||||
|
||||
override suspend fun remove(chatId: ChatId, messageIdentifier: MessageIdentifier) {
|
||||
val prefix = filePrefix(chatId, messageIdentifier)
|
||||
folderFile.listFiles() ?.forEach {
|
||||
if (it.name.startsWith(prefix)) {
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun contains(chatId: ChatId, messageIdentifier: MessageIdentifier): Boolean {
|
||||
val prefix = filePrefix(chatId, messageIdentifier)
|
||||
return folderFile.list() ?.any { it.startsWith(prefix) } == true
|
||||
}
|
||||
}
|
1
cache/content/common/src/main/AndroidManifest.xml
vendored
Normal file
1
cache/content/common/src/main/AndroidManifest.xml
vendored
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.tgbotapi.libraries.cache.content.common"/>
|
19
cache/content/micro_utils/build.gradle
vendored
Normal file
19
cache/content/micro_utils/build.gradle
vendored
Normal file
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,84 @@
|
||||
package dev.inmo.tgbotapi.libraries.cache.media.micro_utils
|
||||
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||
import dev.inmo.tgbotapi.libraries.cache.media.common.MessageContentCache
|
||||
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<Pair<ChatId, MessageIdentifier>, MessageContent>
|
||||
) : MessageContentCache {
|
||||
override suspend fun save(chatId: ChatId, messageId: MessageIdentifier, content: MessageContent): Boolean {
|
||||
return keyValueRepo.runCatching {
|
||||
set(chatId to messageId, content)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
override suspend fun get(chatId: ChatId, messageId: MessageIdentifier): MessageContent? {
|
||||
return keyValueRepo.get(chatId to messageId)
|
||||
}
|
||||
|
||||
override suspend fun contains(chatId: ChatId, messageId: MessageIdentifier): Boolean {
|
||||
return keyValueRepo.contains(chatId to messageId)
|
||||
}
|
||||
|
||||
override suspend fun remove(chatId: ChatId, messageId: MessageIdentifier) {
|
||||
keyValueRepo.unset(chatId to messageId)
|
||||
}
|
||||
}
|
||||
|
||||
val chatIdToMessageIdentifierSerializer = PairSerializer(
|
||||
ChatId.serializer(),
|
||||
MessageIdentifier.serializer()
|
||||
)
|
||||
|
||||
val messageContentSerializer = PolymorphicSerializer<MessageContent>(MessageContent::class)
|
||||
|
||||
inline fun KeyValueRepo<String, String>.asMessageContentCache(
|
||||
serialFormatCreator: (SerializersModule) -> StringFormat = { Json { serializersModule = it } }
|
||||
): StandardKeyValueRepo<Pair<ChatId, MessageIdentifier>, MessageContent> {
|
||||
val serialFormat = serialFormatCreator(MessageContent.serializationModule())
|
||||
return withMapper<Pair<ChatId, MessageIdentifier>, 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<String, String>.asMessageContentCache(
|
||||
serialFormatCreator: (SerializersModule) -> BinaryFormat
|
||||
): StandardKeyValueRepo<Pair<ChatId, MessageIdentifier>, MessageContent> {
|
||||
val serialFormat = serialFormatCreator(MessageContent.serializationModule())
|
||||
return withMapper<Pair<ChatId, MessageIdentifier>, 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<ByteArray, ByteArray>.asMessageContentCache(
|
||||
serialFormatCreator: (SerializersModule) -> BinaryFormat
|
||||
): StandardKeyValueRepo<Pair<ChatId, MessageIdentifier>, MessageContent> {
|
||||
val serialFormat = serialFormatCreator(MessageContent.serializationModule())
|
||||
return withMapper<Pair<ChatId, MessageIdentifier>, MessageContent, ByteArray, ByteArray>(
|
||||
{ serialFormat.encodeToByteArray(chatIdToMessageIdentifierSerializer, this) },
|
||||
{ serialFormat.encodeToByteArray(messageContentSerializer, this) },
|
||||
{ serialFormat.decodeFromByteArray(chatIdToMessageIdentifierSerializer, this) },
|
||||
{ serialFormat.decodeFromByteArray(messageContentSerializer, this) },
|
||||
)
|
||||
}
|
1
cache/content/micro_utils/src/main/AndroidManifest.xml
vendored
Normal file
1
cache/content/micro_utils/src/main/AndroidManifest.xml
vendored
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="dev.inmo.tgbotapi.libraries.cache.content.micro_utils"/>
|
1
cache/media/src/main/AndroidManifest.xml
vendored
1
cache/media/src/main/AndroidManifest.xml
vendored
@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.tgbotapi.libraries.cache.media"/>
|
@ -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.9
|
||||
micro_utils_version=0.9.16
|
||||
exposed_version=0.37.3
|
||||
plagubot_version=0.5.1
|
||||
|
||||
# ANDROID
|
||||
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user