diff --git a/triggers/timer/src/commonMain/kotlin/ButtonsBuilder.kt b/triggers/timer/src/commonMain/kotlin/ButtonsBuilder.kt index f34b0f5..56c2fd8 100644 --- a/triggers/timer/src/commonMain/kotlin/ButtonsBuilder.kt +++ b/triggers/timer/src/commonMain/kotlin/ButtonsBuilder.kt @@ -4,8 +4,11 @@ import com.soywiz.klock.DateTime import com.soywiz.klock.DateTimeTz import com.soywiz.klock.Month import com.soywiz.klock.Year +import dev.inmo.micro_utils.coroutines.runCatchingSafely import dev.inmo.plaguposter.common.SuccessfulSymbol +import dev.inmo.plaguposter.posts.models.Post import dev.inmo.plaguposter.posts.models.PostId +import dev.inmo.tgbotapi.extensions.api.answers.answer import dev.inmo.tgbotapi.extensions.api.edit.edit import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery @@ -38,7 +41,9 @@ object ButtonsBuilder { dataButton(SuccessfulSymbol, "$changeDateDataPrefix $postId $unixMillis") } - suspend fun BehaviourContext.includeKeyboardHandling() { + suspend fun BehaviourContext.includeKeyboardHandling( + onSavePublishingTime: suspend (PostId, DateTime) -> Boolean + ) { fun buildKeyboard( prefix: String, postId: PostId, @@ -180,5 +185,21 @@ object ButtonsBuilder { oldDateTime.offset ) } + + onMessageDataCallbackQuery(Regex("$changeDateDataPrefix .*")) { + val (_, rawPostId, rawDateTimeMillis) = it.data.split(" ") + val currentMillis = rawDateTimeMillis.toLongOrNull() ?: return@onMessageDataCallbackQuery + val currentDateTime = DateTime(currentMillis) + val postId = PostId(rawPostId) + + val success = runCatchingSafely { + onSavePublishingTime(postId, currentDateTime) + }.getOrElse { false } + + answer( + it, + if (success) "Successfully set timer" else "Unable to set timer" + ) + } } } diff --git a/triggers/timer/src/commonMain/kotlin/TimersHandler.kt b/triggers/timer/src/commonMain/kotlin/TimersHandler.kt new file mode 100644 index 0000000..074108a --- /dev/null +++ b/triggers/timer/src/commonMain/kotlin/TimersHandler.kt @@ -0,0 +1,57 @@ +package dev.inmo.plaguposter.triggers.timer + +import com.soywiz.klock.DateTime +import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions +import dev.inmo.micro_utils.coroutines.plus +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.repos.unset +import dev.inmo.plaguposter.posts.models.PostId +import dev.inmo.plaguposter.posts.sending.PostPublisher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +class TimersHandler( + private val timersRepo: TimersRepo, + private val publisher: PostPublisher, + private val scope: CoroutineScope +) { + private var currentPostAndJob: Pair? = null + private val currentJobMutex = Mutex() + + init { + (flowOf(Unit) + timersRepo.onNewValue + timersRepo.onValueRemoved).subscribeSafelyWithoutExceptions(scope) { + refreshPublishingJob() + } + } + + private suspend fun refreshPublishingJob() { + val minimal = timersRepo.getMinimalDateTimePost() + + currentJobMutex.withLock { + if (minimal ?.first == currentPostAndJob ?.first) { + return@withLock + } + + currentPostAndJob ?.second ?.cancel() + + currentPostAndJob = minimal ?.let { (postId, dateTime) -> + postId to scope.launchSafelyWithoutExceptions { + val now = DateTime.now() + val span = dateTime - now + + delay(span.millisecondsLong) + + publisher.publish(postId) + + timersRepo.unset(postId) + + refreshPublishingJob() + } + } + } + } +} diff --git a/triggers/timer/src/commonMain/kotlin/TimersRepo.kt b/triggers/timer/src/commonMain/kotlin/TimersRepo.kt new file mode 100644 index 0000000..f87dd6c --- /dev/null +++ b/triggers/timer/src/commonMain/kotlin/TimersRepo.kt @@ -0,0 +1,9 @@ +package dev.inmo.plaguposter.triggers.timer + +import com.soywiz.klock.DateTime +import dev.inmo.micro_utils.repos.KeyValueRepo +import dev.inmo.plaguposter.posts.models.PostId + +interface TimersRepo : KeyValueRepo { + suspend fun getMinimalDateTimePost(): Pair? +} diff --git a/triggers/timer/src/jvmMain/kotlin/Plugin.kt b/triggers/timer/src/jvmMain/kotlin/Plugin.kt index f0ebf38..d4f719b 100644 --- a/triggers/timer/src/jvmMain/kotlin/Plugin.kt +++ b/triggers/timer/src/jvmMain/kotlin/Plugin.kt @@ -2,8 +2,10 @@ package dev.inmo.plaguposter.triggers.timer import com.soywiz.klock.DateTime import dev.inmo.micro_utils.coroutines.runCatchingSafely +import dev.inmo.micro_utils.repos.set import dev.inmo.plagubot.Plugin import dev.inmo.plaguposter.posts.repo.ReadPostsRepo +import dev.inmo.plaguposter.triggers.timer.repo.ExposedTimersRepo import dev.inmo.tgbotapi.extensions.api.edit.edit import dev.inmo.tgbotapi.extensions.api.send.send import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext @@ -12,15 +14,22 @@ import kotlinx.serialization.json.* import org.jetbrains.exposed.sql.Database import org.koin.core.Koin import org.koin.core.module.Module +import org.koin.dsl.binds object Plugin : Plugin { override fun Module.setupDI(database: Database, params: JsonObject) { + single { ExposedTimersRepo(get(), get(), get()) } binds arrayOf(TimersRepo::class) + single(createdAtStart = true) { TimersHandler(get(), get(), get()) } } override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { val postsRepo = koin.get() + val timersRepo = koin.get() with(ButtonsBuilder) { - includeKeyboardHandling() + includeKeyboardHandling { postId, dateTime -> + timersRepo.set(postId, dateTime) + true + } } onCommand("test") { val reply = it.replyTo ?: return@onCommand diff --git a/triggers/timer/src/jvmMain/kotlin/repo/ExposedTimersRepo.kt b/triggers/timer/src/jvmMain/kotlin/repo/ExposedTimersRepo.kt new file mode 100644 index 0000000..425070c --- /dev/null +++ b/triggers/timer/src/jvmMain/kotlin/repo/ExposedTimersRepo.kt @@ -0,0 +1,62 @@ +package dev.inmo.plaguposter.triggers.timer.repo + +import com.soywiz.klock.DateTime +import dev.inmo.micro_utils.common.firstNotNull +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.pagination.paginate +import dev.inmo.micro_utils.repos.exposed.initTable +import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo +import dev.inmo.micro_utils.repos.unset +import dev.inmo.plaguposter.posts.models.PostId +import dev.inmo.plaguposter.posts.repo.PostsRepo +import dev.inmo.plaguposter.triggers.timer.TimersRepo +import kotlinx.coroutines.CoroutineScope +import org.jetbrains.exposed.sql.Column +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.ISqlExpressionBuilder +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.statements.InsertStatement +import org.jetbrains.exposed.sql.statements.UpdateBuilder +import org.jetbrains.exposed.sql.transactions.transaction + +class ExposedTimersRepo( + database: Database, + postsRepo: PostsRepo, + scope: CoroutineScope +) : TimersRepo, AbstractExposedKeyValueRepo( + database, + "timers" +) { + override val keyColumn = text("post_id") + private val dateTimeColumn = long("date_time") + override val selectById: ISqlExpressionBuilder.(PostId) -> Op = { keyColumn.eq(it.string) } + override val selectByValue: ISqlExpressionBuilder.(DateTime) -> Op = { dateTimeColumn.eq(it.unixMillisLong) } + override val ResultRow.asKey: PostId + get() = PostId(get(keyColumn)) + override val ResultRow.asObject: DateTime + get() = DateTime(get(dateTimeColumn)) + + val postsRepoListeningJob = postsRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(scope) { + unset(it) + } + + init { + initTable() + } + + override fun update(k: PostId, v: DateTime, it: UpdateBuilder) { + it[dateTimeColumn] = v.unixMillisLong + } + + override fun insertKey(k: PostId, v: DateTime, it: InsertStatement) { + it[keyColumn] = k.string + } + + override suspend fun getMinimalDateTimePost(): Pair? = transaction(database) { + selectAll().orderBy(dateTimeColumn).limit(1).firstOrNull() ?.let { + PostId(it[keyColumn]) to DateTime(it[dateTimeColumn]) + } + } +}