diff --git a/cache/admins/build.gradle b/cache/admins/common/build.gradle similarity index 53% rename from cache/admins/build.gradle rename to cache/admins/common/build.gradle index 8a22926..05af6c2 100644 --- a/cache/admins/build.gradle +++ b/cache/admins/common/build.gradle @@ -6,3 +6,12 @@ plugins { apply from: "$mppProjectWithSerializationPresetPath" +kotlin { + sourceSets { + commonMain { + dependencies { + api "dev.inmo:tgbotapi:$tgbotapi_version" + } + } + } +} diff --git a/cache/admins/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/AdminsCacheAPI.kt b/cache/admins/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/AdminsCacheAPI.kt new file mode 100644 index 0000000..6c03f0a --- /dev/null +++ b/cache/admins/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/AdminsCacheAPI.kt @@ -0,0 +1,11 @@ +package dev.inmo.tgbotapi.libraries.cache.admins + +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.ChatMember.abstracts.AdministratorChatMember + +interface AdminsCacheAPI { + suspend fun getChatAdmins(chatId: ChatId): List? + suspend fun isAdmin(userId: UserId, chatId: ChatId): Boolean + + suspend fun settings(): AdminsCacheSettingsAPI +} diff --git a/cache/admins/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/AdminsCacheSettingsAPI.kt b/cache/admins/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/AdminsCacheSettingsAPI.kt new file mode 100644 index 0000000..e960565 --- /dev/null +++ b/cache/admins/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/AdminsCacheSettingsAPI.kt @@ -0,0 +1,41 @@ +package dev.inmo.tgbotapi.libraries.cache.admins + +import com.soywiz.klock.minutes +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.Seconds +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.serialization.Serializable + +@Serializable +data class AdminsCacheSettings( + val refreshSeconds: Seconds = 10.minutes.seconds.toInt(), + /** + * In case this setting set up to true, will request admins list just from time to time instead of refreshing on + * requests + */ + val disableRequestsRefreshMode: Boolean = false +) { + val refreshOnRequests: Boolean + get() = !disableRequestsRefreshMode +} + +interface AdminsCacheSettingsAPI { + suspend fun getChatSettings(chatId: ChatId): AdminsCacheSettings? +} + +interface MutableAdminsCacheSettingsAPI : AdminsCacheSettingsAPI { + val chatSettingsUpdatedFlow: SharedFlow> + + suspend fun setChatSettings(chatId: ChatId, settings: AdminsCacheSettings) +} + +fun AdminsCacheSettingsAPI.asMutable(): MutableAdminsCacheSettingsAPI? = this as? MutableAdminsCacheSettingsAPI + +@Serializable +class StaticAdminsCacheSettingsAPI( + private val settings: Map +) : AdminsCacheSettingsAPI { + override suspend fun getChatSettings(chatId: ChatId): AdminsCacheSettings? = settings[chatId] +} + + diff --git a/cache/admins/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/DefaultAdminsCacheAPI.kt b/cache/admins/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/DefaultAdminsCacheAPI.kt new file mode 100644 index 0000000..55aaff8 --- /dev/null +++ b/cache/admins/common/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/DefaultAdminsCacheAPI.kt @@ -0,0 +1,48 @@ +package dev.inmo.tgbotapi.libraries.cache.admins + +import com.soywiz.klock.DateTime +import dev.inmo.tgbotapi.bot.TelegramBot +import dev.inmo.tgbotapi.extensions.api.chat.get.getChatAdministrators +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.ChatMember.abstracts.AdministratorChatMember +import dev.inmo.tgbotapi.types.UserId +import kotlinx.serialization.Serializable + +interface DefaultAdminsCacheAPIRepo { + suspend fun getChatAdmins(chatId: ChatId): List? + suspend fun setChatAdmins(chatId: ChatId, chatMembers: List) + suspend fun lastUpdate(chatId: ChatId): DateTime? +} + +@Serializable +class DefaultAdminsCacheAPI( + private val bot: TelegramBot, + private val repo: DefaultAdminsCacheAPIRepo, + private val settingsAPI: AdminsCacheSettingsAPI +) : AdminsCacheAPI { + private suspend fun triggerUpdate(chatId: ChatId): List { + val admins = bot.getChatAdministrators(chatId) + repo.setChatAdmins(chatId, admins) + return admins + } + + override suspend fun getChatAdmins(chatId: ChatId): List? { + val settings = settingsAPI.getChatSettings(chatId) + val lastUpdate = repo.lastUpdate(chatId) + return when { + settings == null -> null + settings.refreshOnRequests && + (lastUpdate == null || (DateTime.now() - lastUpdate).seconds > settings.refreshSeconds) -> { + triggerUpdate(chatId) + } + else -> repo.getChatAdmins(chatId) ?: triggerUpdate(chatId) + } + } + + override suspend fun isAdmin(userId: UserId, chatId: ChatId): Boolean = getChatAdmins(chatId) ?.any { + it.user.id == userId + } == true + + override suspend fun settings(): AdminsCacheSettingsAPI = settingsAPI + +} diff --git a/cache/admins/src/main/AndroidManifest.xml b/cache/admins/common/src/main/AndroidManifest.xml similarity index 100% rename from cache/admins/src/main/AndroidManifest.xml rename to cache/admins/common/src/main/AndroidManifest.xml diff --git a/cache/admins/micro_utils/build.gradle b/cache/admins/micro_utils/build.gradle new file mode 100644 index 0000000..d5e74fc --- /dev/null +++ b/cache/admins/micro_utils/build.gradle @@ -0,0 +1,25 @@ +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.admins.common") + } + } + jvmMain { + dependencies { + api "dev.inmo:micro_utils.repos.exposed:$micro_utils_version" + api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version" + api "org.jetbrains.exposed:exposed-jdbc:$exposed_version" + } + } + } +} diff --git a/cache/admins/micro_utils/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/micro_utils/DefaultAdminsCacheAPI.kt b/cache/admins/micro_utils/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/micro_utils/DefaultAdminsCacheAPI.kt new file mode 100644 index 0000000..911ce95 --- /dev/null +++ b/cache/admins/micro_utils/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/micro_utils/DefaultAdminsCacheAPI.kt @@ -0,0 +1,62 @@ +package dev.inmo.tgbotapi.libraries.cache.admins.micro_utils + +import com.soywiz.klock.DateTime +import dev.inmo.micro_utils.coroutines.actor +import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions +import dev.inmo.micro_utils.repos.* +import dev.inmo.tgbotapi.libraries.cache.admins.DefaultAdminsCacheAPIRepo +import dev.inmo.tgbotapi.types.* +import dev.inmo.tgbotapi.types.ChatMember.abstracts.AdministratorChatMember +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlin.coroutines.* + +private sealed class RepoActions { + abstract val toReturn: Continuation +} +private class GetUpdateDateTimeRepoAction( + val chatId: ChatId, + override val toReturn: Continuation +) : RepoActions() +private class GetChatAdminsRepoAction( + val chatId: ChatId, + override val toReturn: Continuation?> +) : RepoActions?>() +private class SetChatAdminsRepoAction( + val chatId: ChatId, + val newValue: List, + override val toReturn: Continuation +) : RepoActions() + +class DefaultAdminsCacheAPIRepo( + private val adminsRepo: KeyValuesRepo, + private val updatesRepo: KeyValueRepo, + private val scope: CoroutineScope +) : DefaultAdminsCacheAPIRepo { + private val actor = scope.actor>(Channel.UNLIMITED) { + safelyWithoutExceptions { + when (it) { + is GetUpdateDateTimeRepoAction -> it.toReturn.resume( + updatesRepo.get(it.chatId) ?.let { DateTime(it.toDouble()) } + ) + is GetChatAdminsRepoAction -> it.toReturn.resume(adminsRepo.getAll(it.chatId)) + is SetChatAdminsRepoAction -> { + adminsRepo.clear(it.chatId) + adminsRepo.set(it.chatId, it.newValue) + updatesRepo.set(it.chatId, DateTime.now().unixMillisLong) + it.toReturn.resume(Unit) + } + } + } + } + + override suspend fun getChatAdmins(chatId: ChatId): List? = suspendCoroutine { + actor.offer(GetChatAdminsRepoAction(chatId, it)) + } + override suspend fun setChatAdmins(chatId: ChatId, chatMembers: List) = suspendCoroutine { + actor.offer(SetChatAdminsRepoAction(chatId, chatMembers, it)) + } + override suspend fun lastUpdate(chatId: ChatId): DateTime? = suspendCoroutine { + actor.offer(GetUpdateDateTimeRepoAction(chatId, it)) + } +} diff --git a/cache/admins/micro_utils/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/micro_utils/DynamicAdminsCacheSettingsAPI.kt b/cache/admins/micro_utils/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/micro_utils/DynamicAdminsCacheSettingsAPI.kt new file mode 100644 index 0000000..756346b --- /dev/null +++ b/cache/admins/micro_utils/src/commonMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/micro_utils/DynamicAdminsCacheSettingsAPI.kt @@ -0,0 +1,31 @@ +package dev.inmo.tgbotapi.libraries.cache.admins.micro_utils + +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.repos.* +import dev.inmo.tgbotapi.libraries.cache.admins.* +import dev.inmo.tgbotapi.types.ChatId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +class DynamicAdminsCacheSettingsAPI( + private val repo: KeyValueRepo, + private val scope: CoroutineScope +) : AdminsCacheSettingsAPI, MutableAdminsCacheSettingsAPI { + override val chatSettingsUpdatedFlow: SharedFlow> + get() = repo.onNewValue.shareIn(scope, SharingStarted.Eagerly) + + override suspend fun setChatSettings(chatId: ChatId, settings: AdminsCacheSettings) { + repo.set(chatId, settings) + } + + override suspend fun getChatSettings(chatId: ChatId): AdminsCacheSettings { + val settings = repo.get(chatId) + return if (settings == null) { + val newSettings = AdminsCacheSettings() + setChatSettings(chatId, newSettings) + newSettings + } else { + settings + } + } +} \ No newline at end of file diff --git a/cache/admins/micro_utils/src/jvmMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/AdminsCacheAPI.kt b/cache/admins/micro_utils/src/jvmMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/AdminsCacheAPI.kt new file mode 100644 index 0000000..b177a84 --- /dev/null +++ b/cache/admins/micro_utils/src/jvmMain/kotlin/dev/inmo/tgbotapi/libraries/cache/admins/AdminsCacheAPI.kt @@ -0,0 +1,64 @@ +package dev.inmo.tgbotapi.libraries.cache.admins + +import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo +import dev.inmo.micro_utils.repos.exposed.onetomany.ExposedOneToManyKeyValueRepo +import dev.inmo.micro_utils.repos.mappers.withMapper +import dev.inmo.tgbotapi.bot.TelegramBot +import dev.inmo.tgbotapi.libraries.cache.admins.micro_utils.DefaultAdminsCacheAPIRepo +import dev.inmo.tgbotapi.libraries.cache.admins.micro_utils.DynamicAdminsCacheSettingsAPI +import dev.inmo.tgbotapi.types.toChatId +import kotlinx.coroutines.CoroutineScope +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.encodeToByteArray +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.statements.api.ExposedBlob + +private val serializationFormat = Cbor + +fun AdminsCacheAPI( + bot: TelegramBot, + database: Database, + scope: CoroutineScope, + repo: dev.inmo.tgbotapi.libraries.cache.admins.DefaultAdminsCacheAPIRepo = DefaultAdminsCacheAPIRepo( + ExposedOneToManyKeyValueRepo( + database, + { long("chatId") }, + { blob("member") }, + "AdminsTable" + ).withMapper( + keyFromToTo = { chatId }, + valueFromToTo = { ExposedBlob(serializationFormat.encodeToByteArray(this)) }, + keyToToFrom = { toChatId() }, + valueToToFrom = { serializationFormat.decodeFromByteArray(bytes) } + ), + ExposedKeyValueRepo( + database, + { long("chatId") }, + { long("datetime") }, + "AdminsUpdatesTimesTable" + ).withMapper( + keyFromToTo = { chatId }, + keyToToFrom = { toChatId() } + ), + scope + ), + settingsAPI: AdminsCacheSettingsAPI = DynamicAdminsCacheSettingsAPI( + ExposedKeyValueRepo( + database, + { long("chatId") }, + { blob("settings") }, + "DynamicAdminsCacheSettingsAPI" + ).withMapper( + keyFromToTo = { chatId }, + valueFromToTo = { ExposedBlob(serializationFormat.encodeToByteArray(this)) }, + keyToToFrom = { toChatId() }, + valueToToFrom = { serializationFormat.decodeFromByteArray(bytes) } + ), + scope + ) +) : AdminsCacheAPI = DefaultAdminsCacheAPI( + bot, + repo, + settingsAPI +) diff --git a/cache/admins/micro_utils/src/main/AndroidManifest.xml b/cache/admins/micro_utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000..589f648 --- /dev/null +++ b/cache/admins/micro_utils/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/gradle.properties b/gradle.properties index dbb3cdb..ce1a615 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,7 @@ github_release_plugin_version=2.2.12 tgbotapi_version=0.32.8 micro_utils_version=0.4.26 +exposed_version=0.29.1 # ANDROID diff --git a/settings.gradle b/settings.gradle index 5754aa8..312d55e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,8 @@ rootProject.name = 'tgbotapi.libraries' String[] includes = [ - ":cache:admins", + ":cache:admins:common", + ":cache:admins:micro_utils", ":cache:media" ]