Compare commits

..

4 Commits

Author SHA1 Message Date
b45bd3192a add dsl preview 2021-06-19 13:57:34 +06:00
c4e18ad25f fixes in architecture and adding playable main 2021-06-19 02:20:47 +06:00
6f17a53146 state to revert 2021-06-19 01:16:39 +06:00
f04f065ac5 start working with fsm 2021-06-18 14:09:02 +06:00
55 changed files with 570 additions and 1196 deletions

View File

@@ -7,7 +7,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 17
java-version: 1.8
- name: Update version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"

2
.gitignore vendored
View File

@@ -1,5 +1,4 @@
.idea
.kotlin
out/*
*.iml
target
@@ -12,6 +11,5 @@ out/
secret.gradle
local.properties
kotlin-js-store/
publishing.sh

View File

@@ -1,5 +1,6 @@
buildscript {
repositories {
jcenter()
google()
mavenCentral()
mavenLocal()
@@ -7,35 +8,21 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
}
}
plugins {
id "com.gradleup.nmcp.aggregation" version "$nmcp_version"
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
nmcpAggregation {
centralPortal {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
validationTimeout = Duration.ofHours(4)
publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED"
}
publishAllProjectsProbablyBreakingProjectIsolation()
}
}
allprojects {
repositories {
mavenLocal()
jcenter()
mavenCentral()
google()
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
maven { url "https://kotlin.bintray.com/kotlinx" }
}
}

View File

@@ -1,9 +1,10 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppJavaWithJsProjectPath"
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {

View File

@@ -2,13 +2,13 @@ package dev.inmo.tgbotapi.libraries.cache.admins
import dev.inmo.tgbotapi.extensions.utils.asGroupContentMessage
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMember
import dev.inmo.tgbotapi.types.ChatMember.abstracts.AdministratorChatMember
import dev.inmo.tgbotapi.types.message.abstracts.GroupContentMessage
import dev.inmo.tgbotapi.types.message.abstracts.Message
interface AdminsCacheAPI {
suspend fun getChatAdmins(chatId: IdChatIdentifier): List<AdministratorChatMember>?
suspend fun isAdmin(chatId: IdChatIdentifier, userId: UserId): Boolean = getChatAdmins(chatId) ?.any {
suspend fun getChatAdmins(chatId: ChatId): List<AdministratorChatMember>?
suspend fun isAdmin(chatId: ChatId, userId: UserId): Boolean = getChatAdmins(chatId) ?.any {
it.user.id == userId
} == true
suspend fun sentByAdmin(groupContentMessage: GroupContentMessage<*>): Boolean

View File

@@ -1,9 +1,8 @@
package dev.inmo.tgbotapi.libraries.cache.admins
import korlibs.time.minutes
import dev.inmo.tgbotapi.types.IdChatIdentifier
import com.soywiz.klock.minutes
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.Seconds
import korlibs.time.seconds
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.serialization.Serializable
@@ -16,30 +15,27 @@ data class AdminsCacheSettings(
*/
val disableRequestsRefreshMode: Boolean = false
) {
val refreshOnCacheCalls: Boolean
get() = !disableRequestsRefreshMode
@Deprecated("Renamed", ReplaceWith("refreshOnCacheCalls"))
val refreshOnRequests: Boolean
get() = refreshOnCacheCalls
get() = !disableRequestsRefreshMode
}
interface AdminsCacheSettingsAPI {
suspend fun getChatSettings(chatId: IdChatIdentifier): AdminsCacheSettings?
suspend fun getChatSettings(chatId: ChatId): AdminsCacheSettings?
}
interface MutableAdminsCacheSettingsAPI : AdminsCacheSettingsAPI {
val chatSettingsUpdatedFlow: SharedFlow<Pair<IdChatIdentifier, AdminsCacheSettings>>
val chatSettingsUpdatedFlow: SharedFlow<Pair<ChatId, AdminsCacheSettings>>
suspend fun setChatSettings(chatId: IdChatIdentifier, settings: AdminsCacheSettings)
suspend fun setChatSettings(chatId: ChatId, settings: AdminsCacheSettings)
}
fun AdminsCacheSettingsAPI.asMutable(): MutableAdminsCacheSettingsAPI? = this as? MutableAdminsCacheSettingsAPI
@Serializable
class StaticAdminsCacheSettingsAPI(
private val settings: Map<IdChatIdentifier, AdminsCacheSettings>
private val settings: Map<ChatId, AdminsCacheSettings>
) : AdminsCacheSettingsAPI {
override suspend fun getChatSettings(chatId: IdChatIdentifier): AdminsCacheSettings? = settings[chatId]
override suspend fun getChatSettings(chatId: ChatId): AdminsCacheSettings? = settings[chatId]
}

View File

@@ -1,44 +0,0 @@
package dev.inmo.tgbotapi.libraries.cache.admins
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.behaviour_builder.*
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.ChatMemberUpdatedFilterByChat
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onChatMemberUpdated
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.SimpleFilter
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.marker_factories.ByChatChatMemberUpdatedMarkerFactory
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.marker_factories.MarkerFactory
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMember
import dev.inmo.tgbotapi.types.chat.member.ChatMemberUpdated
import dev.inmo.tgbotapi.types.update.abstracts.Update
import kotlinx.coroutines.Job
suspend fun BehaviourContext.activateAdminsChangesListening(
repo: DefaultAdminsCacheAPIRepo,
initialFilter: SimpleFilter<ChatMemberUpdated>? = null,
markerFactory: MarkerFactory<ChatMemberUpdated, Any> = ByChatChatMemberUpdatedMarkerFactory
): Job {
val me = getMe()
return onChatMemberUpdated(initialFilter, markerFactory = markerFactory) {
when {
it.oldChatMemberState is AdministratorChatMember && it.newChatMemberState !is AdministratorChatMember ||
it.newChatMemberState is AdministratorChatMember && it.oldChatMemberState !is AdministratorChatMember -> {
updateAdmins(
it.chat.id,
repo,
me
)
}
}
}
}
suspend fun BehaviourContext.activateAdminsChangesListening(
repo: DefaultAdminsCacheAPIRepo,
allowedChats: List<IdChatIdentifier>
) = activateAdminsChangesListening(
repo,
{
it.chat.id in allowedChats
}
)

View File

@@ -1,37 +0,0 @@
package dev.inmo.tgbotapi.libraries.cache.admins
import dev.inmo.tgbotapi.abstracts.FromUser
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.SimpleFilter
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.message.abstracts.Message
fun AdminsChecker(
adminsCacheAPI: AdminsCacheAPI
): SimpleFilter<Pair<IdChatIdentifier, UserId>> = SimpleFilter {
adminsCacheAPI.isAdmin(it.first, it.second)
}
fun <T> AdminsChecker(
adminsCacheAPI: AdminsCacheAPI,
mapper: (T) -> Pair<IdChatIdentifier, UserId>
): SimpleFilter<T> {
val baseChecker = AdminsChecker(adminsCacheAPI)
return SimpleFilter<T> {
baseChecker(mapper(it))
}
}
fun MessageAdminsChecker(
adminsCacheAPI: AdminsCacheAPI
) = SimpleFilter<Message> {
adminsCacheAPI.isAdmin(it)
}
fun AdminsChecker(
adminsCacheAPI: AdminsCacheAPI,
chatId: IdChatIdentifier
) = SimpleFilter<FromUser> {
adminsCacheAPI.isAdmin(chatId, it.from.id)
}

View File

@@ -1,20 +1,18 @@
package dev.inmo.tgbotapi.libraries.cache.admins
import korlibs.time.DateTime
import com.soywiz.klock.DateTime
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.chat.members.getChatMember
import dev.inmo.tgbotapi.extensions.api.chat.get.getChatAdministrators
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.chat.ExtendedBot
import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMember
import dev.inmo.tgbotapi.types.ChatMember.abstracts.AdministratorChatMember
import dev.inmo.tgbotapi.types.message.abstracts.*
import korlibs.time.seconds
import kotlinx.serialization.Serializable
interface DefaultAdminsCacheAPIRepo {
suspend fun getChatAdmins(chatId: IdChatIdentifier): List<AdministratorChatMember>?
suspend fun setChatAdmins(chatId: IdChatIdentifier, chatMembers: List<AdministratorChatMember>)
suspend fun lastUpdate(chatId: IdChatIdentifier): DateTime?
suspend fun getChatAdmins(chatId: ChatId): List<AdministratorChatMember>?
suspend fun setChatAdmins(chatId: ChatId, chatMembers: List<AdministratorChatMember>)
suspend fun lastUpdate(chatId: ChatId): DateTime?
}
@Serializable
@@ -30,37 +28,28 @@ class DefaultAdminsCacheAPI(
bot.getMe().also { botInfo = it }
}
override suspend fun getChatAdmins(chatId: IdChatIdentifier): List<AdministratorChatMember>? {
private suspend fun triggerUpdate(chatId: ChatId): List<AdministratorChatMember> {
val botInfo = getBotInfo()
val admins = bot.getChatAdministrators(chatId).filter {
botInfo.id != it.user.id
}
repo.setChatAdmins(chatId, admins)
return admins
}
override suspend fun getChatAdmins(chatId: ChatId): List<AdministratorChatMember>? {
val settings = settingsAPI.getChatSettings(chatId)
val lastUpdate = repo.lastUpdate(chatId)
return when {
settings == null -> null
settings.refreshOnCacheCalls &&
settings.refreshOnRequests &&
(lastUpdate == null || (DateTime.now() - lastUpdate).seconds > settings.refreshSeconds) -> {
bot.updateAdmins(chatId, repo, getBotInfo())
triggerUpdate(chatId)
}
else -> repo.getChatAdmins(chatId) ?: bot.updateAdmins(chatId, repo, getBotInfo())
else -> repo.getChatAdmins(chatId) ?: triggerUpdate(chatId)
}
}
override suspend fun isAdmin(chatId: IdChatIdentifier, userId: UserId): Boolean {
val settings = settingsAPI.getChatSettings(chatId)
val lastUpdate = repo.lastUpdate(chatId)
return when {
settings == null -> return false
settings.refreshOnCacheCalls && (lastUpdate == null || (DateTime.now() - lastUpdate).seconds > settings.refreshSeconds) -> {
bot.updateAdmins(chatId, repo, getBotInfo())
}
else -> {
val chatAdmins = repo.getChatAdmins(chatId)
if (chatAdmins == null) {
return bot.getChatMember(chatId, userId) is AdministratorChatMember
}
chatAdmins
}
}.any { it.user.id == userId }
}
override suspend fun sentByAdmin(groupContentMessage: GroupContentMessage<*>): Boolean {
return when (groupContentMessage) {
is AnonymousGroupContentMessage -> true
@@ -73,4 +62,5 @@ class DefaultAdminsCacheAPI(
}
override suspend fun settings(): AdminsCacheSettingsAPI = settingsAPI
}

View File

@@ -1,43 +1,20 @@
package dev.inmo.tgbotapi.libraries.cache.admins
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.UserId
import dev.inmo.tgbotapi.types.message.abstracts.*
suspend inline fun AdminsCacheAPI.isAdmin(message: Message) = when (message) {
suspend fun AdminsCacheAPI.verifyMessageFromAdmin(message: ContentMessage<*>) = when (message) {
is CommonGroupContentMessage<*> -> isAdmin(message.chat.id, message.user.id)
is AnonymousGroupContentMessage<*> -> true
else -> false
}
suspend inline fun AdminsCacheAPI.verifyMessageFromAdmin(message: Message) = isAdmin(message)
suspend inline fun <R : Any> AdminsCacheAPI.doIfAdmin(
chatId: IdChatIdentifier,
userId: UserId,
block: () -> R
) = if(isAdmin(chatId, userId)) {
block()
} else {
null
}
suspend inline fun <R : Any> AdminsCacheAPI.doIfAdmin(
message: Message,
block: () -> R
) = if(isAdmin(message)) {
block()
} else {
null
}
suspend inline fun <R> ContentMessage<*>.doIfAdmin(adminsCacheAPI: AdminsCacheAPI, block: () -> R): R? {
val verified = adminsCacheAPI.isAdmin(this)
suspend fun <R> ContentMessage<*>.doAfterVerification(adminsCacheAPI: AdminsCacheAPI, block: suspend () -> R): R? {
val verified = adminsCacheAPI.verifyMessageFromAdmin(this)
return if (verified) {
block()
} else {
null
}
}
suspend inline fun <R> ContentMessage<*>.doAfterVerification(adminsCacheAPI: AdminsCacheAPI, block: () -> R) = doIfAdmin(adminsCacheAPI, block)

View File

@@ -1,21 +0,0 @@
package dev.inmo.tgbotapi.libraries.cache.admins
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.extensions.api.bot.getMe
import dev.inmo.tgbotapi.extensions.api.chat.get.getChatAdministrators
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.chat.ExtendedBot
import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMember
internal suspend fun TelegramBot.updateAdmins(
chatId: IdChatIdentifier,
repo: DefaultAdminsCacheAPIRepo,
botInfo: ExtendedBot? = null
): List<AdministratorChatMember> {
val botInfo = botInfo ?: getMe()
val admins = getChatAdministrators(chatId).filter {
botInfo.id != it.user.id
}
repo.setChatAdmins(chatId, admins)
return admins
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.tgbotapi.libraries.cache.admins"/>

View File

@@ -1,9 +1,10 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppJavaWithJsProjectPath"
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {

View File

@@ -1,91 +1,62 @@
package dev.inmo.tgbotapi.libraries.cache.admins.micro_utils
import korlibs.time.DateTime
import dev.inmo.micro_utils.coroutines.*
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.chat.member.AdministratorChatMember
import kotlinx.coroutines.CompletableDeferred
import dev.inmo.tgbotapi.types.ChatMember.abstracts.AdministratorChatMember
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.channels.Channel
import kotlin.coroutines.*
private sealed class RepoActions<T> {
abstract val deferred: CompletableDeferred<T>
abstract val toReturn: Continuation<T>
}
private class GetUpdateDateTimeRepoAction(
val chatId: IdChatIdentifier,
override val deferred: CompletableDeferred<DateTime?>
val chatId: ChatId,
override val toReturn: Continuation<DateTime?>
) : RepoActions<DateTime?>()
private class GetChatAdminsRepoAction(
val chatId: IdChatIdentifier,
override val deferred: CompletableDeferred<List<AdministratorChatMember>?>
val chatId: ChatId,
override val toReturn: Continuation<List<AdministratorChatMember>?>
) : RepoActions<List<AdministratorChatMember>?>()
private class SetChatAdminsRepoAction(
val chatId: IdChatIdentifier,
val chatId: ChatId,
val newValue: List<AdministratorChatMember>,
override val deferred: CompletableDeferred<Unit>
override val toReturn: Continuation<Unit>
) : RepoActions<Unit>()
class DefaultAdminsCacheAPIRepoImpl(
private val adminsRepo: KeyValuesRepo<IdChatIdentifier, AdministratorChatMember>,
private val updatesRepo: KeyValueRepo<IdChatIdentifier, MilliSeconds>,
class DefaultAdminsCacheAPIRepo(
private val adminsRepo: KeyValuesRepo<ChatId, AdministratorChatMember>,
private val updatesRepo: KeyValueRepo<ChatId, MilliSeconds>,
private val scope: CoroutineScope
) : DefaultAdminsCacheAPIRepo {
private val actor = scope.actorAsync<RepoActions<*>>(Channel.UNLIMITED) {
safelyWithoutExceptions(
{ e ->
it.deferred.completeExceptionally(e)
}
) {
private val actor = scope.actor<RepoActions<*>>(Channel.UNLIMITED) {
safelyWithoutExceptions {
when (it) {
is GetUpdateDateTimeRepoAction -> it.deferred.complete(
is GetUpdateDateTimeRepoAction -> it.toReturn.resume(
updatesRepo.get(it.chatId) ?.let { DateTime(it.toDouble()) }
)
is GetChatAdminsRepoAction -> it.deferred.complete(adminsRepo.getAll(it.chatId))
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.deferred.complete(Unit)
it.toReturn.resume(Unit)
}
}
}
}
override suspend fun getChatAdmins(chatId: IdChatIdentifier): List<AdministratorChatMember>? {
val deferred = CompletableDeferred<List<AdministratorChatMember>?>()
actor.trySend(
GetChatAdminsRepoAction(chatId, deferred)
).onFailure {
deferred.completeExceptionally(it ?: IllegalStateException("Something went wrong when tried to add getChatAdmins action"))
}
return deferred.await()
override suspend fun getChatAdmins(chatId: ChatId): List<AdministratorChatMember>? = suspendCoroutine {
actor.offer(GetChatAdminsRepoAction(chatId, it))
}
override suspend fun setChatAdmins(chatId: IdChatIdentifier, chatMembers: List<AdministratorChatMember>) {
val deferred = CompletableDeferred<Unit>()
actor.trySend(
SetChatAdminsRepoAction(chatId, chatMembers, deferred)
).onFailure {
deferred.completeExceptionally(it ?: IllegalStateException("Something went wrong when tried to add setChatAdmins action"))
}
return deferred.await()
override suspend fun setChatAdmins(chatId: ChatId, chatMembers: List<AdministratorChatMember>) = suspendCoroutine<Unit> {
actor.offer(SetChatAdminsRepoAction(chatId, chatMembers, it))
}
override suspend fun lastUpdate(chatId: IdChatIdentifier): DateTime? {
val deferred = CompletableDeferred<DateTime?>()
actor.trySend(
GetUpdateDateTimeRepoAction(chatId, deferred)
).onFailure {
deferred.completeExceptionally(it ?: IllegalStateException("Something went wrong when tried to add lastUpdate action"))
}
return deferred.await()
override suspend fun lastUpdate(chatId: ChatId): DateTime? = suspendCoroutine {
actor.offer(GetUpdateDateTimeRepoAction(chatId, it))
}
}
fun DefaultAdminsCacheAPIRepo(
adminsRepo: KeyValuesRepo<IdChatIdentifier, AdministratorChatMember>,
updatesRepo: KeyValueRepo<IdChatIdentifier, MilliSeconds>,
scope: CoroutineScope
) = DefaultAdminsCacheAPIRepoImpl(adminsRepo, updatesRepo, scope)

View File

@@ -3,22 +3,22 @@ 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.IdChatIdentifier
import dev.inmo.tgbotapi.types.ChatId
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.*
class DynamicAdminsCacheSettingsAPI(
private val repo: KeyValueRepo<IdChatIdentifier, AdminsCacheSettings>,
private val repo: KeyValueRepo<ChatId, AdminsCacheSettings>,
private val scope: CoroutineScope
) : AdminsCacheSettingsAPI, MutableAdminsCacheSettingsAPI {
override val chatSettingsUpdatedFlow: SharedFlow<Pair<IdChatIdentifier, AdminsCacheSettings>>
override val chatSettingsUpdatedFlow: SharedFlow<Pair<ChatId, AdminsCacheSettings>>
get() = repo.onNewValue.shareIn(scope, SharingStarted.Eagerly)
override suspend fun setChatSettings(chatId: IdChatIdentifier, settings: AdminsCacheSettings) {
override suspend fun setChatSettings(chatId: ChatId, settings: AdminsCacheSettings) {
repo.set(chatId, settings)
}
override suspend fun getChatSettings(chatId: IdChatIdentifier): AdminsCacheSettings {
override suspend fun getChatSettings(chatId: ChatId): AdminsCacheSettings {
val settings = repo.get(chatId)
return if (settings == null) {
val newSettings = AdminsCacheSettings()
@@ -28,4 +28,4 @@ class DynamicAdminsCacheSettingsAPI(
settings
}
}
}
}

View File

@@ -1,83 +1,81 @@
package dev.inmo.tgbotapi.libraries.cache.admins
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.micro_utils.repos.exposed.onetomany.ExposedKeyValuesRepo
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.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.libraries.cache.admins.micro_utils.DefaultAdminsCacheAPIRepoImpl
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.*
import dev.inmo.tgbotapi.types.chat.member.*
import dev.inmo.tgbotapi.types.ChatMember.AdministratorChatMemberImpl
import dev.inmo.tgbotapi.types.ChatMember.CreatorChatMember
import dev.inmo.tgbotapi.types.ChatMember.abstracts.AdministratorChatMember
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.*
import kotlinx.serialization.cbor.Cbor
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
val telegramAdminsSerializationFormat = Json {
private val serializationFormat = Json {
ignoreUnknownKeys = true
serializersModule = SerializersModule {
polymorphic(AdministratorChatMember::class) {
subclass(AdministratorChatMemberImpl::class, AdministratorChatMemberImpl.serializer())
subclass(OwnerChatMember::class, OwnerChatMember.serializer())
subclass(CreatorChatMember::class, CreatorChatMember.serializer())
}
contextual(AdministratorChatMember::class, PolymorphicSerializer(AdministratorChatMember::class))
}
}
fun BehaviourContext.createAdminsCacheAPI(database: Database) = AdminsCacheAPI(this, database, this)
fun TelegramBot.createAdminsCacheAPI(
fun AdminsCacheAPI(
bot: TelegramBot,
database: Database,
scope: CoroutineScope,
defaultAdminsCacheAPIRepo: DefaultAdminsCacheAPIRepo = DefaultAdminsCacheAPIRepoImpl(
ExposedKeyValuesRepo(
scope: CoroutineScope
) : AdminsCacheAPI = DefaultAdminsCacheAPI(
bot,
DefaultAdminsCacheAPIRepo(
ExposedOneToManyKeyValueRepo(
database,
{ long("chatId") },
{ text("member") },
"AdminsTable"
).withMapper<IdChatIdentifier, AdministratorChatMember, Long, String>(
keyFromToTo = { chatId.long },
valueFromToTo = { telegramAdminsSerializationFormat.encodeToString(AdministratorChatMember.serializer(), this) },
).withMapper<ChatId, AdministratorChatMember, Identifier, String>(
keyFromToTo = { chatId },
valueFromToTo = { serializationFormat.encodeToString(this) },
keyToToFrom = { toChatId() },
valueToToFrom = { telegramAdminsSerializationFormat.decodeFromString(AdministratorChatMember.serializer(), this) }
valueToToFrom = { serializationFormat.decodeFromString(this) }
),
ExposedKeyValueRepo(
database,
{ long("chatId") },
{ long("datetime") },
"AdminsUpdatesTimesTable"
).withMapper<IdChatIdentifier, Long, Long, Long>(
keyFromToTo = { chatId.long },
).withMapper<ChatId, Long, Identifier, Long>(
keyFromToTo = { chatId },
valueFromToTo = { this },
keyToToFrom = { toChatId() },
valueToToFrom = { this }
),
scope
),
adminsCacheSettingsAPI: AdminsCacheSettingsAPI = DynamicAdminsCacheSettingsAPI(
DynamicAdminsCacheSettingsAPI(
ExposedKeyValueRepo(
database,
{ long("chatId") },
{ text("settings") },
"DynamicAdminsCacheSettingsAPI"
).withMapper<IdChatIdentifier, AdminsCacheSettings, Long, String>(
keyFromToTo = { chatId.long },
valueFromToTo = { telegramAdminsSerializationFormat.encodeToString(AdminsCacheSettings.serializer() , this) },
).withMapper<ChatId, AdminsCacheSettings, Identifier, String>(
keyFromToTo = { chatId },
valueFromToTo = { serializationFormat.encodeToString(this) },
keyToToFrom = { toChatId() },
valueToToFrom = { telegramAdminsSerializationFormat.decodeFromString(AdminsCacheSettings.serializer() , this) }
valueToToFrom = { serializationFormat.decodeFromString(this) }
),
scope
)
) = DefaultAdminsCacheAPI(this, defaultAdminsCacheAPIRepo, adminsCacheSettingsAPI)
fun AdminsCacheAPI(
bot: TelegramBot,
database: Database,
scope: CoroutineScope
) : AdminsCacheAPI = bot.createAdminsCacheAPI(
database,
scope
)
fun BehaviourContext.AdminsCacheAPI(database: Database) = AdminsCacheAPI(this, database, this)

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.tgbotapi.libraries.cache.admins.micro_utils"/>

View File

@@ -1,9 +1,10 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppJavaWithJsProjectPath"
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {

View File

@@ -1,107 +1,53 @@
package dev.inmo.tgbotapi.libraries.cache.admins
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.micro_utils.repos.exposed.onetomany.ExposedKeyValuesRepo
import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.database
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.libraries.cache.admins.micro_utils.DefaultAdminsCacheAPIRepoImpl
import dev.inmo.tgbotapi.libraries.cache.admins.micro_utils.DynamicAdminsCacheSettingsAPI
import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.chat.member.AdministratorChatMember
import kotlinx.coroutines.*
import dev.inmo.tgbotapi.types.ChatId
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.*
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin
import org.koin.core.module.Module
import org.koin.core.qualifier.named
import org.koin.core.scope.Scope
import org.koin.dsl.binds
val Scope.adminsPlugin: AdminsPlugin?
get() = getOrNull()
val Koin.adminsPlugin: AdminsPlugin?
get() = getOrNull()
val Map<String, Any>.adminsPlugin: AdminsPlugin?
get() = get("admins") as? AdminsPlugin
@Serializable
class AdminsPlugin : Plugin {
class AdminsPlugin(
private val chatsSettings: Map<ChatId, AdminsCacheSettings>? = null
) : Plugin {
@Transient
private val globalAdminsCacheAPI = MutableStateFlow<AdminsCacheAPI?>(null)
@Transient
private val databaseToAdminsCacheAPI = mutableMapOf<Database, MutableStateFlow<AdminsCacheAPI?>>()
private val mutex = Mutex()
@Deprecated("Will be removed soon due to its redundancy")
suspend fun adminsAPI(database: Database): AdminsCacheAPI {
val flow = mutex.withLock {
databaseToAdminsCacheAPI.getOrPut(database){ MutableStateFlow(null) }
return when (chatsSettings) {
null -> {
val flow = mutex.withLock {
databaseToAdminsCacheAPI.getOrPut(database){ MutableStateFlow(null) }
}
flow.first { it != null }!!
}
else -> globalAdminsCacheAPI.first { it != null }!!
}
return flow.filterNotNull().first()
}
override fun Module.setupDI(config: JsonObject) {
single { this@AdminsPlugin }
val scopeQualifier = named("admins plugin scope")
single(scopeQualifier) { CoroutineScope(Dispatchers.IO + SupervisorJob()) }
single<DefaultAdminsCacheAPIRepo> {
DefaultAdminsCacheAPIRepoImpl(
ExposedKeyValuesRepo(
database,
{ long("chatId") },
{ text("member") },
"AdminsTable"
).withMapper<IdChatIdentifier, AdministratorChatMember, Long, String>(
keyFromToTo = { chatId.long },
valueFromToTo = { telegramAdminsSerializationFormat.encodeToString(this) },
keyToToFrom = { toChatId() },
valueToToFrom = { telegramAdminsSerializationFormat.decodeFromString(this) }
),
ExposedKeyValueRepo(
database,
{ long("chatId") },
{ long("datetime") },
"AdminsUpdatesTimesTable"
).withMapper<IdChatIdentifier, Long, Long, Long>(
keyFromToTo = { chatId.long },
valueFromToTo = { this },
keyToToFrom = { toChatId() },
valueToToFrom = { this }
),
get(scopeQualifier)
)
}
single<AdminsCacheSettingsAPI> {
DynamicAdminsCacheSettingsAPI(
ExposedKeyValueRepo(
database,
{ long("chatId") },
{ text("settings") },
"DynamicAdminsCacheSettingsAPI"
).withMapper<IdChatIdentifier, AdminsCacheSettings, Long, String>(
keyFromToTo = { chatId.long },
valueFromToTo = { telegramAdminsSerializationFormat.encodeToString(this) },
keyToToFrom = { toChatId() },
valueToToFrom = { telegramAdminsSerializationFormat.decodeFromString(this) }
),
get(scopeQualifier)
)
}
single { DefaultAdminsCacheAPI(get(), get(), get()) } binds arrayOf(
AdminsCacheAPI::class
)
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
with(koin) {
activateAdminsChangesListening(
get()
)
override suspend fun BehaviourContext.invoke(database: Database, params: Map<String, Any>) {
when (chatsSettings) {
null -> {
mutex.withLock {
val flow = databaseToAdminsCacheAPI.getOrPut(database){ MutableStateFlow(null) }
if (flow.value == null) {
flow.value = AdminsCacheAPI(database)
}
}
}
else -> mutex.withLock {
globalAdminsCacheAPI.value = AdminsCacheAPI(database)
}
}
}
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.tgbotapi.libraries.cache.admins.plagubot"/>

View File

@@ -1,150 +0,0 @@
package dev.inmo.tgbotapi.libraries.cache.media.common
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.DeleteMessage
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.IdChatIdentifier
import dev.inmo.tgbotapi.types.media.*
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.DocumentContent
import dev.inmo.tgbotapi.types.message.content.MediaContent
import dev.inmo.tgbotapi.types.message.content.MessageContent
import dev.inmo.tgbotapi.utils.asInput
import io.ktor.utils.io.core.Input
class DefaultMessageContentCache<K>(
private val bot: TelegramBot,
private val filesRefreshingChatId: IdChatIdentifier,
private val simpleMessageContentCache: MessagesSimpleCache<K>,
private val mediaFileActualityChecker: MediaFileActualityChecker = MediaFileActualityChecker.WithDelay(
MediaFileActualityChecker.Default(filesRefreshingChatId)
),
private val messagesFilesCache: MessagesFilesCache<K> = InMemoryMessagesFilesCache()
) : MessageContentCache<K> {
override suspend fun save(k: K, content: MessageContent) {
when (content) {
is MediaContent -> {
val extendedInfo = bot.execute(
GetFile(content.media.fileId)
)
val allocator = bot.execute(
DownloadFileStream(
extendedInfo.filePath
)
)
save(k, content, extendedInfo.fileName) {
allocator.invoke().asInput()
}
}
else -> simpleMessageContentCache.set(k, content)
}
}
override suspend fun save(
k: K,
content: MediaContent,
filename: String,
inputAllocator: suspend () -> Input
) {
simpleMessageContentCache.set(k, content)
runCatching {
messagesFilesCache.set(k, filename, inputAllocator)
}.onFailure {
simpleMessageContentCache.remove(k)
}.onSuccess {
with(mediaFileActualityChecker) {
bot.saved(content)
}
}
}
override suspend fun sendAndSave(
k: K,
filename: String,
inputAllocator: () -> Input
): DocumentContent {
val sentDocument = bot.execute(
SendDocument(
filesRefreshingChatId,
MultipartFile(filename, inputAllocator),
)
)
save(k, sentDocument.content, filename, inputAllocator)
runCatching {
bot.execute(
DeleteMessage(
sentDocument.chat.id,
sentDocument.messageId
)
)
}
return sentDocument.content
}
override suspend fun get(k: K): MessageContent? {
val savedSimpleContent = simpleMessageContentCache.get(k) ?: return null
if (savedSimpleContent is MediaContent && !with(mediaFileActualityChecker) { bot.isActual(savedSimpleContent) }) {
val savedFileContentAllocator = messagesFilesCache.get(k) ?: error("Unexpected absence of $k file for content ($simpleMessageContentCache)")
val newContent = bot.execute(
when (savedSimpleContent.asTelegramMedia()) {
is TelegramMediaAnimation -> SendAnimation(
filesRefreshingChatId,
savedFileContentAllocator,
disableNotification = true
)
is TelegramMediaAudio -> SendAudio(
filesRefreshingChatId,
savedFileContentAllocator,
disableNotification = true
)
is TelegramMediaVideo -> SendVideo(
filesRefreshingChatId,
savedFileContentAllocator,
disableNotification = true
)
is TelegramMediaDocument -> SendDocument(
filesRefreshingChatId,
savedFileContentAllocator,
disableNotification = true
)
is TelegramMediaPhoto -> SendPhoto(
filesRefreshingChatId,
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: IdChatIdentifier,
simpleMessageContentCache: MessagesSimpleCache<String> = InMemoryMessagesSimpleCache(),
mediaFileActualityChecker: MediaFileActualityChecker = MediaFileActualityChecker.WithDelay(
MediaFileActualityChecker.Default(filesRefreshingChatId)
),
messagesFilesCache: MessagesFilesCache<String> = InMemoryMessagesFilesCache()
) = DefaultMessageContentCache(bot, filesRefreshingChatId, simpleMessageContentCache, mediaFileActualityChecker, messagesFilesCache)
}
}

View File

@@ -1,56 +0,0 @@
package dev.inmo.tgbotapi.libraries.cache.media.common
import korlibs.time.DateTime
import korlibs.time.milliseconds
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.DeleteMessage
import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MilliSeconds
import dev.inmo.tgbotapi.types.message.content.MediaContent
fun interface MediaFileActualityChecker {
suspend fun TelegramBot.isActual(mediaContent: MediaContent): Boolean
suspend fun TelegramBot.saved(mediaContent: MediaContent) {}
class Default(
private val checkingChatId: IdChatIdentifier
) : MediaFileActualityChecker {
override suspend fun TelegramBot.isActual(mediaContent: MediaContent): Boolean {
return runCatching {
execute(mediaContent.createResend(checkingChatId)).also { sentMessage ->
execute(DeleteMessage(sentMessage.chat.id, sentMessage.messageId))
}
}.isSuccess
}
}
class WithDelay(
private val underhoodChecker: MediaFileActualityChecker,
private val checkingDelay: MilliSeconds = 24 * 60 * 60 * 1000L // one day
) : MediaFileActualityChecker {
private val fileIdChecksMap = mutableMapOf<FileId, DateTime>()
private val checkingDelayTimeSpan = checkingDelay.milliseconds
override suspend fun TelegramBot.isActual(mediaContent: MediaContent): Boolean {
val now = DateTime.now()
val lastCheck = fileIdChecksMap[mediaContent.media.fileId]
return if (lastCheck == null || now - lastCheck > checkingDelayTimeSpan) {
with(underhoodChecker) {
isActual(mediaContent)
}.also {
if (it) {
fileIdChecksMap[mediaContent.media.fileId] = now
}
}
} else {
true
}
}
override suspend fun TelegramBot.saved(mediaContent: MediaContent) {
fileIdChecksMap[mediaContent.media.fileId] = DateTime.now()
}
}
}

View File

@@ -1,26 +0,0 @@
package dev.inmo.tgbotapi.libraries.cache.media.common
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.DocumentContent
import dev.inmo.tgbotapi.types.message.content.MediaContent
import dev.inmo.tgbotapi.types.message.content.MessageContent
import io.ktor.utils.io.core.Input
interface MessageContentCache<K> {
suspend fun save(k: K, content: MessageContent)
suspend fun save(
k: K,
content: MediaContent,
filename: String,
inputAllocator: suspend () -> Input
)
suspend fun sendAndSave(
k: K,
filename: String,
inputAllocator: () -> Input
): DocumentContent
suspend fun get(k: K): MessageContent?
suspend fun contains(k: K): Boolean
suspend fun remove(k: K)
}

View File

@@ -1,41 +0,0 @@
package dev.inmo.tgbotapi.libraries.cache.media.common
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
import io.ktor.utils.io.core.*
interface MessagesFilesCache<K> {
suspend fun set(k: K, filename: String, inputAllocator: suspend () -> Input)
suspend fun get(k: K): MultipartFile?
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<K> : MessagesFilesCache<K> {
private val map = mutableMapOf<K, MultipartFile>()
override suspend fun set(k: K, filename: String, inputAllocator: suspend () -> Input) {
val input = inputAllocator()
map[k] = MultipartFile(
filename
) {
input
}
}
override suspend fun get(k: K): MultipartFile? {
return map[k]
}
override suspend fun remove(k: K) {
map.remove(k)
}
override suspend fun contains(k: K): Boolean {
return map.contains(k)
}
}

View File

@@ -1,53 +0,0 @@
package dev.inmo.tgbotapi.libraries.cache.media.common
import dev.inmo.tgbotapi.types.message.content.MessageContent
interface MessagesSimpleCache<K> {
suspend fun set(k: K, content: MessageContent)
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<K> : MessagesSimpleCache<K> {
private val map = mutableMapOf<K, MessageContent>()
override suspend fun set(
k: K,
content: MessageContent
) {
map[k] = content
}
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)
}
}

View File

@@ -1,71 +0,0 @@
package dev.inmo.tgbotapi.libraries.cache.media.common
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
import io.ktor.utils.io.core.Input
import io.ktor.utils.io.core.copyTo
import io.ktor.utils.io.streams.*
import java.io.File
class InFilesMessagesFilesCache<K>(
private val folderFile: File,
private val filePrefixBuilder: (K) -> String
) : MessagesFilesCache<K> {
private val K.multipartFile: MultipartFile?
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 MultipartFile(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().inputStream().use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
}
override suspend fun get(k: K): MultipartFile? {
return k.multipartFile
}
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<String>(
folderFile
) { it }
}
}

View File

@@ -1,18 +0,0 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppJavaWithJsProjectPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api "dev.inmo:micro_utils.repos.common:$micro_utils_version"
api project(":tgbotapi.libraries.cache.content.common")
}
}
}
}

View File

@@ -1,95 +0,0 @@
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.MessagesSimpleCache
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.MessageId
import dev.inmo.tgbotapi.types.message.content.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<K>(
private val keyValueRepo: KeyValueRepo<K, MessageContent>
) : MessagesSimpleCache<K> {
override suspend fun set(k: K, content: MessageContent) {
keyValueRepo.set(k, content)
}
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)
}
}
val chatIdToMessageIdentifierSerializer = PairSerializer(
ChatId.serializer(),
MessageId.serializer()
)
val messageContentSerializer = PolymorphicSerializer<MessageContent>(MessageContent::class)
inline fun <K> KeyValueRepo<K, MessageContent>.asMessageContentCache() = SimpleKeyValueMessageContentCache(this)
inline fun KeyValueRepo<String, String>.asMessageContentCache(
serialFormatCreator: (SerializersModule) -> StringFormat = { Json { serializersModule = it } }
): StandardKeyValueRepo<Pair<ChatId, MessageId>, MessageContent> {
val serialFormat = serialFormatCreator(MessageContent.serializationModule())
return withMapper<Pair<ChatId, MessageId>, 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, MessageId>, MessageContent> {
val serialFormat = serialFormatCreator(MessageContent.serializationModule())
return withMapper<Pair<ChatId, MessageId>, 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, MessageId>, MessageContent> {
val serialFormat = serialFormatCreator(MessageContent.serializationModule())
return withMapper<Pair<ChatId, MessageId>, MessageContent, ByteArray, ByteArray>(
{ serialFormat.encodeToByteArray(chatIdToMessageIdentifierSerializer, this) },
{ serialFormat.encodeToByteArray(messageContentSerializer, this) },
{ serialFormat.decodeFromByteArray(chatIdToMessageIdentifierSerializer, this) },
{ serialFormat.decodeFromByteArray(messageContentSerializer, this) },
)
}

8
cache/media/build.gradle vendored Normal file
View File

@@ -0,0 +1,8 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppProjectWithSerializationPresetPath"

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.tgbotapi.libraries.cache.media"/>

View File

@@ -0,0 +1,40 @@
apply plugin: 'com.getkeepsafe.dexcount'
android {
compileSdkVersion "$android_compileSdkVersion".toInteger()
buildToolsVersion "$android_buildToolsVersion"
defaultConfig {
minSdkVersion "$android_minSdkVersion".toInteger()
targetSdkVersion "$android_compileSdkVersion".toInteger()
versionCode "${android_code_version}".toInteger()
versionName "$version"
}
buildTypes {
release {
minifyEnabled false
}
debug {
debuggable true
}
}
packagingOptions {
exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module'
exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module'
exclude 'META-INF/kotlinx-serialization-properties.kotlin_module'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
}

View File

@@ -13,7 +13,11 @@ allprojects {
projectByName(name)
}
mppJavaWithJsProjectPath = "${rootProject.projectDir.absolutePath}/mppJavaWithJsProject.gradle"
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"
defaultAndroidSettingsPresetPath = "${rootProject.projectDir.absolutePath}/defaultAndroidSettings.gradle"
publishGradlePath = "${rootProject.projectDir.absolutePath}/publish.gradle"
}

View File

@@ -1,17 +1,17 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppJavaWithJsProjectPath"
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api "dev.inmo:tgbotapi.core:$tgbotapi_version"
api "dev.inmo:micro_utils.coroutines:$micro_utils_version"
}
}
}
}

View File

@@ -0,0 +1,17 @@
package dev.inmo.tgbotapi.libraries.fsm.core
sealed interface State {
val context: Any
}
/**
* Use this state as parent of your state in case you want to avoid saving of this state in queue for [context] if this
* queue is not empty
*/
interface ImmediateOrNeverState : State
/**
* Use this state as parent of your state in case you want to keep saving of this state in queue for [context] if this
* queue is not empty
*/
interface QueueableState : State

View File

@@ -0,0 +1,15 @@
package dev.inmo.tgbotapi.libraries.fsm.core
import kotlin.reflect.KClass
class StateHandlerHolder<I : State>(
private val inputKlass: KClass<I>,
private val strict: Boolean = false,
private val delegateTo: StatesHandler<I>
) : StatesHandler<State> {
fun checkHandleable(state: State) = state::class == inputKlass || (!strict && inputKlass.isInstance(state))
override suspend fun StatesMachine.handleState(state: State): State? {
return delegateTo.run { handleState(state as I) }
}
}

View File

@@ -0,0 +1,5 @@
package dev.inmo.tgbotapi.libraries.fsm.core
fun interface StatesHandler<I : State> {
suspend fun StatesMachine.handleState(state: I): State?
}

View File

@@ -0,0 +1,46 @@
package dev.inmo.tgbotapi.libraries.fsm.core
import dev.inmo.micro_utils.coroutines.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.asFlow
private suspend fun <I : State> StatesMachine.launchStateHandling(
state: State,
handlers: List<StateHandlerHolder<out I>>
): State? {
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
handleState(state)
}
}
class StatesMachine (
private val statesManager: StatesManager,
private val handlers: List<StateHandlerHolder<*>>
) : StatesHandler<State> {
override suspend fun StatesMachine.handleState(state: State): State? = launchStateHandling(state, handlers)
fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
val statePerformer: suspend (State) -> Unit = { state: State ->
val newState = launchStateHandling(state, handlers)
if (newState != null) {
statesManager.update(state, newState)
} else {
statesManager.endChain(state)
}
}
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it) }
}
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it.second) }
}
statesManager.getActiveStates().forEach {
launch { statePerformer(it) }
}
}
suspend fun startChain(state: State) {
statesManager.startChain(state)
}
}

View File

@@ -0,0 +1,92 @@
package dev.inmo.tgbotapi.libraries.fsm.core
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
interface StatesManager {
val onChainStateUpdated: Flow<Pair<State, State>>
val onStartChain: Flow<State>
val onEndChain: Flow<State>
/**
* Must set current set using [State.context]
*/
suspend fun update(old: State, new: State)
/**
* Starts chain with [state] as first [State]. May returns false in case of [State.context] of [state] is already
* busy by the other [State]
*/
suspend fun startChain(state: State)
/**
* Ends chain with context from [state]. In case when [State.context] of [state] is absent, [state] should be just
* ignored
*/
suspend fun endChain(state: State)
suspend fun getActiveStates(): List<State>
}
/**
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
* new state by using [endChain] with that state
*/
class InMemoryStatesManager(
private val onContextsConflictResolver: suspend (old: State, new: State, currentNew: State) -> Boolean = { _, _, _ -> true }
) : StatesManager {
private val _onChainStateUpdated = MutableSharedFlow<Pair<State, State>>(0)
override val onChainStateUpdated: Flow<Pair<State, State>> = _onChainStateUpdated.asSharedFlow()
private val _onStartChain = MutableSharedFlow<State>(0)
override val onStartChain: Flow<State> = _onStartChain.asSharedFlow()
private val _onEndChain = MutableSharedFlow<State>(0)
override val onEndChain: Flow<State> = _onEndChain.asSharedFlow()
private val contextsToStates = mutableMapOf<Any, State>()
private val mapMutex = Mutex()
override suspend fun update(old: State, new: State) = mapMutex.withLock {
when {
contextsToStates[old.context] != old -> return@withLock
old.context == new.context || !contextsToStates.containsKey(new.context) -> {
contextsToStates[old.context] = new
_onChainStateUpdated.emit(old to new)
}
else -> {
val stateOnNewOneContext = contextsToStates.getValue(new.context)
if (onContextsConflictResolver(old, new, stateOnNewOneContext)) {
endChainWithoutLock(stateOnNewOneContext)
contextsToStates.remove(old.context)
contextsToStates[new.context] = new
_onChainStateUpdated.emit(old to new)
}
}
}
}
override suspend fun startChain(state: State) = mapMutex.withLock {
if (!contextsToStates.containsKey(state.context)) {
contextsToStates[state.context] = state
_onStartChain.emit(state)
}
}
private suspend fun endChainWithoutLock(state: State) {
if (contextsToStates[state.context] == state) {
contextsToStates.remove(state.context)
_onEndChain.emit(state)
}
}
override suspend fun endChain(state: State) {
mapMutex.withLock {
endChainWithoutLock(state)
}
}
override suspend fun getActiveStates(): List<State> = contextsToStates.values.toList()
}

View File

@@ -0,0 +1,35 @@
package dev.inmo.tgbotapi.libraries.fsm.core.dsl
import dev.inmo.tgbotapi.libraries.fsm.core.*
import kotlin.reflect.KClass
class FSMBuilder(
var statesManager: StatesManager = InMemoryStatesManager()
) {
private var states = mutableListOf<StateHandlerHolder<*>>()
fun <I : State> add(kClass: KClass<I>, handler: StatesHandler<I>) {
states.add(StateHandlerHolder(kClass, false, handler))
}
fun <I : State> addStrict(kClass: KClass<I>, handler: StatesHandler<I>) {
states.add(StateHandlerHolder(kClass, true, handler))
}
fun build() = StatesMachine(
statesManager,
states.toList()
)
}
inline fun <reified I : State> FSMBuilder.onStateOrSubstate(handler: StatesHandler<I>) {
add(I::class, handler)
}
inline fun <reified I : State> FSMBuilder.strictlyOn(handler: StatesHandler<I>) {
addStrict(I::class, handler)
}
fun buildFSM(
block: FSMBuilder.() -> Unit
): StatesMachine = FSMBuilder().apply(block).build()

View File

@@ -0,0 +1,54 @@
import dev.inmo.tgbotapi.libraries.fsm.core.*
import dev.inmo.tgbotapi.libraries.fsm.core.dsl.buildFSM
import dev.inmo.tgbotapi.libraries.fsm.core.dsl.strictlyOn
import kotlinx.coroutines.*
import kotlin.random.Random
import kotlin.test.Test
sealed interface TrafficLightState : ImmediateOrNeverState {
val trafficLightNumber: Int
override val context: Int
get() = trafficLightNumber
}
data class GreenCommon(override val trafficLightNumber: Int) : TrafficLightState
data class YellowCommon(override val trafficLightNumber: Int) : TrafficLightState
data class RedCommon(override val trafficLightNumber: Int) : TrafficLightState
class PlayableMain {
fun test() {
runBlocking {
val countOfTrafficLights = 10
val initialStates = (0 until countOfTrafficLights).map {
when (0/*Random.nextInt(3)*/) {
0 -> GreenCommon(it)
1 -> YellowCommon(it)
else -> RedCommon(it)
}
}
val statesManager = InMemoryStatesManager()
val machine = buildFSM {
strictlyOn<GreenCommon> {
delay(1000L)
YellowCommon(it.context).also(::println)
}
strictlyOn<YellowCommon> {
delay(1000L)
RedCommon(it.context).also(::println)
}
strictlyOn<RedCommon> {
delay(1000L)
GreenCommon(it.context).also(::println)
}
this.statesManager = statesManager
}
initialStates.forEach { machine.startChain(it) }
val scope = CoroutineScope(Dispatchers.Default)
machine.start(scope).join()
}
}
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.tgbotapi.libraries.fsm.core"/>

View File

@@ -1,17 +1,17 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
}
apply from: "$mppJavaWithJsProjectPath"
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api "dev.inmo:tgbotapi.core:$tgbotapi_version"
api project(":tgbotapi.libraries.fsm.core")
}
}
}
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.tgbotapi.libraries.fsm.tgbotapi"/>

View File

@@ -1,28 +1,38 @@
org.gradle.jvmargs=-Xmx512m
kotlin.code.style=official
org.gradle.parallel=true
kotlin.js.generate.externals=true
kotlin.incremental=true
kotlin.incremental.js=true
android.useAndroidX=true
android.enableJetifier=true
kotlin_version=1.5.10
kotlin_serialisation_core_version=1.2.1
kotlin_coroutines_version=1.5.0
kotlin_version=2.2.20
kotlin_serialisation_core_version=1.9.0
github_release_plugin_version=2.2.12
github_release_plugin_version=2.5.2
tgbotapi_version=0.35.0
micro_utils_version=0.5.12
exposed_version=0.32.1
plagubot_version=0.3.0
tgbotapi_version=30.0.0
micro_utils_version=0.26.6
exposed_version=0.61.0
plagubot_version=10.9.0
# ANDROID
nmcp_version=1.2.0
android_minSdkVersion=21
android_compileSdkVersion=30
android_buildToolsVersion=30.0.3
dexcount_version=2.0.0
junit_version=4.12
test_ext_junit_version=1.1.2
espresso_core=3.3.0
# Dokka
dokka_version=2.1.0
dokka_version=1.4.32
# Project data
group=dev.inmo
version=0.27.0
version=0.0.10
android_code_version=9

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

26
mppAndroidProject.gradle Normal file
View File

@@ -0,0 +1,26 @@
project.version = "$version"
project.group = "$group"
apply from: "$publishGradlePath"
kotlin {
android {
publishAllLibraryVariants()
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
}
}
apply from: "$defaultAndroidSettingsPresetPath"

30
mppJavaProject.gradle Normal file
View File

@@ -0,0 +1,30 @@
project.version = "$version"
project.group = "$group"
apply from: "$publishGradlePath"
kotlin {
jvm {
compilations.main.kotlinOptions.useIR = true
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
}
}
}
}

View File

@@ -4,17 +4,14 @@ project.group = "$group"
apply from: "$publishGradlePath"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
jvm()
js (IR) {
browser()
nodejs()
}
android {
publishAllLibraryVariants()
}
sourceSets {
commonMain {
@@ -37,12 +34,17 @@ kotlin {
jsTest {
dependencies {
implementation kotlin('test-js')
implementation kotlin('test-junit')
}
}
androidTest {
dependencies {
implementation kotlin('test-junit')
implementation "androidx.test.ext:junit:$test_ext_junit_version"
implementation "androidx.test.espresso:espresso-core:$espresso_core"
}
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
apply from: "$defaultAndroidSettingsPresetPath"

View File

@@ -1,7 +1,7 @@
apply plugin: 'maven-publish'
task javadocsJar(type: Jar) {
archiveClassifier = 'javadoc'
classifier = 'javadoc'
}
publishing {
@@ -38,81 +38,14 @@ publishing {
}
}
repositories {
if ((project.hasProperty('GITHUBPACKAGES_USER') || System.getenv('GITHUBPACKAGES_USER') != null) && (project.hasProperty('GITHUBPACKAGES_PASSWORD') || System.getenv('GITHUBPACKAGES_PASSWORD') != null)) {
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/TelegramBotApiLibraries")
credentials {
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
}
}
if ((project.hasProperty('INMONEXUS_USER') || System.getenv('INMONEXUS_USER') != null) && (project.hasProperty('INMONEXUS_PASSWORD') || System.getenv('INMONEXUS_PASSWORD') != null)) {
maven {
name = "InmoNexus"
url = uri("https://nexus.inmo.dev/repository/maven-releases/")
credentials {
username = project.hasProperty('INMONEXUS_USER') ? project.property('INMONEXUS_USER') : System.getenv('INMONEXUS_USER')
password = project.hasProperty('INMONEXUS_PASSWORD') ? project.property('INMONEXUS_PASSWORD') : System.getenv('INMONEXUS_PASSWORD')
}
}
}
if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != null) && (project.hasProperty('SONATYPE_PASSWORD') || System.getenv('SONATYPE_PASSWORD') != null)) {
maven {
name = "sonatype"
url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")
credentials {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
}
maven {
name = "GithubPackages"
url = uri("https://maven.pkg.github.com/InsanusMokrassar/TelegramBotApiLibraries")
credentials {
username = project.hasProperty('GITHUBPACKAGES_USER') ? project.property('GITHUBPACKAGES_USER') : System.getenv('GITHUBPACKAGES_USER')
password = project.hasProperty('GITHUBPACKAGES_PASSWORD') ? project.property('GITHUBPACKAGES_PASSWORD') : System.getenv('GITHUBPACKAGES_PASSWORD')
}
}
}
}
}
if (project.hasProperty("signing.gnupg.keyName")) {
apply plugin: 'signing'
signing {
useGpgCmd()
sign publishing.publications
}
task signAll {
tasks.withType(Sign).forEach {
dependsOn(it)
}
}
// Workaround to make android sign operations depend on signing tasks
project.getTasks().withType(AbstractPublishToMaven.class).configureEach {
def signingTasks = project.getTasks().withType(Sign.class)
mustRunAfter(signingTasks)
}
// Workaround to make test tasks use sign
project.getTasks().withType(Sign.class).configureEach { signTask ->
def withoutSign = (signTask.name.startsWith("sign") ? signTask.name.minus("sign") : signTask.name)
def pubName = withoutSign.endsWith("Publication") ? withoutSign.substring(0, withoutSign.length() - "Publication".length()) : withoutSign
// These tasks only exist for native targets, hence findByName() to avoid trying to find them for other targets
// Task ':linkDebugTest<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def debugTestTask = tasks.findByName("linkDebugTest$pubName")
if (debugTestTask != null) {
signTask.mustRunAfter(debugTestTask)
}
// Task ':compileTestKotlin<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def testTask = tasks.findByName("compileTestKotlin$pubName")
if (testTask != null) {
signTask.mustRunAfter(testTask)
}
}
}

View File

@@ -1 +0,0 @@
{"licenses":[{"id":"MIT","title":"MIT License","url":"https://opensource.org/licenses/MIT"}],"mavenConfig":{"name":"${project.name}","description":"${project.name}","url":"https://github.com/InsanusMokrassar/TelegramBotApiLibraries","vcsUrl":"https://github.com/InsanusMokrassar/TelegramBotApiLibraries.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/TelegramBotApiLibraries"},{"name":"InmoNexus","url":"https://nexus.inmo.dev/repository/maven-releases/"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}}}

1
publish_config.kpsb Normal file
View File

@@ -0,0 +1 @@
{"licenses":[{"id":"MIT","title":"MIT License","url":"https://opensource.org/licenses/MIT"}],"mavenConfig":{"name":"${project.name}","description":"${project.name}","url":"https://github.com/InsanusMokrassar/TelegramBotApiLibraries","vcsUrl":"https://github.com/InsanusMokrassar/TelegramBotApiLibraries.git","includeGpgSigning":false,"developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/TelegramBotApiLibraries"}]}}

View File

@@ -1,6 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

View File

@@ -1,36 +0,0 @@
package dev.inmo.tgbotapi.libraries.resender
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MediaGroupId
import dev.inmo.tgbotapi.types.MessageId
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.abstracts.Message
import dev.inmo.tgbotapi.types.message.abstracts.PossiblyMediaGroupMessage
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
import kotlinx.serialization.Serializable
@Serializable
data class MessageMetaInfo(
@Serializable(FullChatIdentifierSerializer::class)
val chatId: IdChatIdentifier,
val messageId: MessageId,
val group: MediaGroupId? = null
) {
val metaInfo: Message.MetaInfo
get() = Message.MetaInfo(chatId, messageId)
}
fun Message.asMessageMetaInfos(): List<MessageMetaInfo> {
return if (this is ContentMessage<*>) {
(content as? MediaGroupContent<*>) ?.group ?.map {
MessageMetaInfo(it.sourceMessage.chat.id, it.sourceMessage.messageId, it.sourceMessage.mediaGroupId)
}
} else {
null
} ?: listOf(MessageMetaInfo(chat.id, messageId, (this as? PossiblyMediaGroupMessage<*>) ?.mediaGroupId))
}
operator fun MessageMetaInfo.Companion.invoke(
message: Message
) = MessageMetaInfo(message.chat.id, message.messageId, (message as? PossiblyMediaGroupMessage<*>) ?.mediaGroupId)

View File

@@ -1,160 +0,0 @@
package dev.inmo.tgbotapi.libraries.resender
import dev.inmo.micro_utils.common.applyDiff
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.ForwardMessage
import dev.inmo.tgbotapi.requests.ForwardMessages
import dev.inmo.tgbotapi.requests.send.CopyMessage
import dev.inmo.tgbotapi.requests.send.CopyMessages
import dev.inmo.tgbotapi.requests.send.media.SendMediaGroup
import dev.inmo.tgbotapi.types.ChatIdentifier
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.mediaCountInMediaGroup
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.MediaGroupPartContent
class MessagesResender(
private val bot: TelegramBot,
private val cacheChatId: ChatIdentifier
) {
suspend fun resend(
targetChatId: IdChatIdentifier,
messagesInfo: List<MessageMetaInfo>,
onBetweenMessages: suspend (sent: List<MessageMetaInfo>, toBeSent: List<MessageMetaInfo>) -> Unit
): List<Pair<MessageMetaInfo, MessageMetaInfo>> {
val messagesWithOrders = messagesInfo.mapIndexed { i, messageInfo -> messageInfo to i }.toMap()
val ordersWithMessagesGroups = messagesInfo.groupBy { it.group }.flatMap { (group, list) ->
if (group == null) {
list.map {
messagesWithOrders.getValue(it) to listOf(it)
}
} else {
listOf(messagesWithOrders.getValue(list.first()) to list)
}
}.sortedBy { it.first }
val sent = mutableListOf<MessageMetaInfo>()
val leftToSend = ordersWithMessagesGroups.map { it.second }.toMutableList()
return ordersWithMessagesGroups.flatMap { (_, contents) ->
val sourceMessagesToSentMessages = mutableListOf<Pair<MessageMetaInfo, MessageMetaInfo>>()
onBetweenMessages(sent.toList(), leftToSend.flatten())
when {
contents.size == 1 -> {
val messageInfo = contents.first()
runCatching {
MessageMetaInfo(
targetChatId,
bot.execute(
CopyMessage(
toChatId = targetChatId,
fromChatId = messageInfo.chatId,
messageId = messageInfo.messageId,
)
)
)
}.onFailure { _ ->
runCatching {
bot.execute(
ForwardMessage(
toChatId = targetChatId,
fromChatId = messageInfo.chatId,
messageId = messageInfo.messageId
)
)
}.onSuccess {
MessageMetaInfo(
targetChatId,
bot.execute(
CopyMessage(
toChatId = targetChatId,
fromChatId = it.chat.id,
messageId = it.messageId
)
)
)
}
}.getOrNull() ?.let {
sourceMessagesToSentMessages.add(messageInfo to it)
}
}
else -> {
val resultContents = contents.mapNotNull {
it to (
bot.execute(
ForwardMessage(
toChatId = cacheChatId,
fromChatId = it.chatId,
messageId = it.messageId
)
) as? ContentMessage<*> ?: return@mapNotNull null)
}.mapNotNull { (src, forwardedMessage) ->
val forwardedMessageAsMediaPartMessage = forwardedMessage.takeIf {
it.content is MediaGroupPartContent
} ?.let {
it as ContentMessage<MediaGroupPartContent>
}
src to (forwardedMessageAsMediaPartMessage ?: null.also { _ ->
sourceMessagesToSentMessages.add(
src to MessageMetaInfo(
targetChatId,
bot.execute(
CopyMessage(
toChatId = targetChatId,
fromChatId = forwardedMessage.chat.id,
messageId = forwardedMessage.messageId
)
)
)
)
} ?: return@mapNotNull null)
}
resultContents.singleOrNull() ?.also { (src, it) ->
sourceMessagesToSentMessages.add(
src to MessageMetaInfo(
targetChatId,
bot.execute(
CopyMessage(
toChatId = targetChatId,
fromChatId = it.chat.id,
messageId = it.messageId
)
)
)
)
} ?: resultContents.chunked(mediaCountInMediaGroup.last).forEach {
bot.execute(
SendMediaGroup<MediaGroupPartContent>(
targetChatId,
it.map { it.second.content.toMediaGroupMemberTelegramMedia() }
)
).content.group.mapIndexed { i, partWrapper ->
it.getOrNull(i) ?.let {
sourceMessagesToSentMessages.add(
it.first to MessageMetaInfo(
partWrapper.sourceMessage.chat.id,
partWrapper.sourceMessage.messageId,
partWrapper.sourceMessage.mediaGroupId
)
)
}
}
}
}
}
leftToSend.takeIf { it.isNotEmpty() } ?.removeAt(0) ?.also {
sent.addAll(it)
}
sourceMessagesToSentMessages.toList()
}
}
suspend fun resend(
targetChatId: IdChatIdentifier,
messagesInfo: List<MessageMetaInfo>
): List<Pair<MessageMetaInfo, MessageMetaInfo>> = resend(targetChatId, messagesInfo) { _, _ -> }
}

View File

@@ -4,17 +4,16 @@ String[] includes = [
":cache:admins:common",
":cache:admins:micro_utils",
":cache:admins:plagubot",
":cache:media",
":cache:content:common",
":cache:content:micro_utils",
":resender",
":fsm:core",
":fsm:tgbotapi"
]
includes.each { originalName ->
String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replace(":", File.separator)}"
String projectName = "${rootProject.name}${originalName.replace(":", ".")}"
String projectDirectory = "${rootProject.projectDir.getAbsolutePath()}${originalName.replaceAll(":", File.separator)}"
String projectName = "${rootProject.name}${originalName.replaceAll(":", ".")}"
String projectIdentifier = ":${projectName}"
include projectIdentifier
ProjectDescriptor project = project(projectIdentifier)