Merge pull request #11 from InsanusMokrassar/0.0.8

0.0.8
This commit is contained in:
InsanusMokrassar 2022-12-15 11:00:27 +06:00 committed by GitHub
commit 5cd3a6fb35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 713 additions and 18 deletions

View File

@ -1,15 +1,27 @@
package dev.inmo.plaguposter.common package dev.inmo.plaguposter.common
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ChatConfig( data class ChatConfig(
@SerialName("targetChat") @SerialName("targetChat")
val targetChatId: ChatId, @Serializable(FullChatIdentifierSerializer::class)
val targetChatId: IdChatIdentifier,
@SerialName("sourceChat") @SerialName("sourceChat")
val sourceChatId: ChatId, @Serializable(FullChatIdentifierSerializer::class)
val sourceChatId: IdChatIdentifier,
@SerialName("cacheChat") @SerialName("cacheChat")
val cacheChatId: ChatId @Serializable(FullChatIdentifierSerializer::class)
) val cacheChatId: IdChatIdentifier
) {
fun check(chatId: IdChatIdentifier) = when (chatId) {
targetChatId,
sourceChatId,
cacheChatId -> true
else -> false
}
}

View File

@ -6,10 +6,20 @@ import dev.inmo.kslog.common.logger
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.serialization.json.JsonObject
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module
object CommonPlugin : Plugin { object CommonPlugin : Plugin {
private val Log = logger private val Log = logger
override fun Module.setupDI(database: Database, params: JsonObject) {
single { CoroutineScope(Dispatchers.Default + SupervisorJob()) }
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val config = koin.get<ChatConfig>() val config = koin.get<ChatConfig>()

View File

@ -10,5 +10,4 @@ android.enableJetifier=true
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.0.7 version=0.0.8
android_code_version=7

View File

@ -3,11 +3,11 @@
kotlin = "1.7.22" kotlin = "1.7.22"
kotlin-serialization = "1.4.1" kotlin-serialization = "1.4.1"
plagubot = "3.2.0" plagubot = "3.2.1"
tgbotapi = "4.2.1" tgbotapi = "4.2.1"
microutils = "0.16.0" microutils = "0.16.1"
kslog = "0.5.4" kslog = "0.5.4"
krontab = "0.8.4" krontab = "0.8.5"
tgbotapi-libraries = "0.6.5" tgbotapi-libraries = "0.6.5"
plagubot-plugins = "0.6.4" plagubot-plugins = "0.6.4"

View File

@ -7,4 +7,6 @@ import kotlin.jvm.JvmInline
@JvmInline @JvmInline
value class PostId( value class PostId(
val string: String val string: String
) ) {
override fun toString(): String = string
}

View File

@ -17,8 +17,8 @@ import dev.inmo.tgbotapi.types.message.content.MediaGroupPartContent
class PostPublisher( class PostPublisher(
private val bot: TelegramBot, private val bot: TelegramBot,
private val postsRepo: PostsRepo, private val postsRepo: PostsRepo,
private val cachingChatId: ChatId, private val cachingChatId: IdChatIdentifier,
private val targetChatId: ChatId, private val targetChatId: IdChatIdentifier,
private val deleteAfterPosting: Boolean = true private val deleteAfterPosting: Boolean = true
) { ) {
suspend fun publish(postId: PostId) { suspend fun publish(postId: PostId) {

View File

@ -101,6 +101,7 @@ object Plugin : Plugin {
} }
val post = postsRepo.getById(postId) ?: return false val post = postsRepo.getById(postId) ?: return false
ratingsRepo.set(postId, Rating(0.0))
for (content in post.content) { for (content in post.content) {
runCatchingSafely { runCatchingSafely {
val sent = send( val sent = send(
@ -140,7 +141,7 @@ object Plugin : Plugin {
} }
} }
postsRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(this) { postId -> ratingsRepo.onValueRemoved.subscribeSafelyWithoutExceptions(this) { postId ->
detachPoll(postId) detachPoll(postId)
} }

View File

@ -1,6 +1,7 @@
package dev.inmo.plaguposter.ratings.exposed package dev.inmo.plaguposter.ratings.exposed
import dev.inmo.micro_utils.pagination.utils.optionallyReverse import dev.inmo.micro_utils.pagination.utils.optionallyReverse
import dev.inmo.micro_utils.repos.exposed.initTable
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.models.Rating import dev.inmo.plaguposter.ratings.models.Rating
@ -24,6 +25,10 @@ class ExposedRatingsRepo (
override val ResultRow.asObject: Rating override val ResultRow.asObject: Rating
get() = get(ratingsColumn).let(::Rating) get() = get(ratingsColumn).let(::Rating)
init {
initTable()
}
override fun update(k: PostId, v: Rating, it: UpdateBuilder<Int>) { override fun update(k: PostId, v: Rating, it: UpdateBuilder<Int>) {
it[ratingsColumn] = v.double it[ratingsColumn] = v.double
} }

View File

@ -15,6 +15,9 @@ dependencies {
api project(":plaguposter.posts_registrar") api project(":plaguposter.posts_registrar")
api project(":plaguposter.triggers.command") api project(":plaguposter.triggers.command")
api project(":plaguposter.triggers.selector_with_timer") api project(":plaguposter.triggers.selector_with_timer")
api project(":plaguposter.triggers.timer")
api project(":plaguposter.triggers.timer.disablers.autoposts")
api project(":plaguposter.triggers.timer.disablers.ratings")
api project(":plaguposter.ratings") api project(":plaguposter.ratings")
api project(":plaguposter.ratings.source") api project(":plaguposter.ratings.source")
api project(":plaguposter.ratings.selector") api project(":plaguposter.ratings.selector")

View File

@ -7,15 +7,19 @@
}, },
"botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678", "botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
"plugins": [ "plugins": [
"dev.inmo.plagubot.plugins.inline.queries.Plugin",
"dev.inmo.plaguposter.posts.Plugin", "dev.inmo.plaguposter.posts.Plugin",
"dev.inmo.plaguposter.posts.registrar.Plugin", "dev.inmo.plaguposter.posts.registrar.Plugin",
"dev.inmo.plaguposter.ratings.Plugin", "dev.inmo.plaguposter.ratings.Plugin",
"dev.inmo.plaguposter.ratings.source.Plugin", "dev.inmo.plaguposter.ratings.source.Plugin",
"dev.inmo.plaguposter.ratings.selector.Plugin", "dev.inmo.plaguposter.ratings.selector.Plugin",
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin", "dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plagubot.plugins.inline.queries.Plugin",
"dev.inmo.plaguposter.triggers.command.Plugin", "dev.inmo.plaguposter.triggers.command.Plugin",
"dev.inmo.plaguposter.posts.panel.Plugin" "dev.inmo.plaguposter.posts.panel.Plugin",
"dev.inmo.plaguposter.common.CommonPlugin",
"dev.inmo.plaguposter.triggers.timer.Plugin",
"dev.inmo.plaguposter.triggers.timer.disablers.ratings.Plugin",
"dev.inmo.plaguposter.triggers.timer.disablers.autoposts.Plugin"
], ],
"posts": { "posts": {
"chats": { "chats": {

View File

@ -12,6 +12,9 @@ String[] includes = [
":triggers:command", ":triggers:command",
":triggers:selector_with_timer", ":triggers:selector_with_timer",
":triggers:selector_with_scheduling", ":triggers:selector_with_scheduling",
":triggers:timer",
":triggers:timer:disablers:ratings",
":triggers:timer:disablers:autoposts",
":inlines", ":inlines",
// ":settings", // ":settings",
":runner" ":runner"

View File

@ -0,0 +1,8 @@
package dev.inmo.plaguposter.triggers.selector_with_timer
import com.soywiz.klock.DateTime
import dev.inmo.plaguposter.posts.models.PostId
fun interface AutopostFilter {
suspend fun check(postId: PostId, dateTime: DateTime): Boolean
}

View File

@ -34,9 +34,12 @@ object Plugin : Plugin {
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val publisher = koin.get<PostPublisher>() val publisher = koin.get<PostPublisher>()
val selector = koin.get<Selector>() val selector = koin.get<Selector>()
koin.get<Config>().krontab.asFlow().subscribeSafelyWithoutExceptions(this) { val filters = koin.getAll<AutopostFilter>().distinct()
selector.take(now = it).forEach { postId -> koin.get<Config>().krontab.asFlow().subscribeSafelyWithoutExceptions(this) { dateTime ->
publisher.publish(postId) selector.take(now = dateTime).forEach { postId ->
if (filters.all { it.check(postId, dateTime) }) {
publisher.publish(postId)
}
} }
} }
} }

View File

@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":plaguposter.common")
api project(":plaguposter.posts")
api project(":plaguposter.posts.panel")
}
}
}
}

View File

@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":plaguposter.common")
api project(":plaguposter.triggers.timer")
api project(":plaguposter.triggers.selector_with_timer")
}
}
}
}

View File

@ -0,0 +1 @@
package dev.inmo.plaguposter.triggers.timer.disablers.autoposts

View File

@ -0,0 +1,27 @@
package dev.inmo.plaguposter.triggers.timer.disablers.autoposts
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithRandomQualifier
import dev.inmo.micro_utils.koin.singleWithRandomQualifierAndBinds
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.repos.unset
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.plaguposter.triggers.selector_with_timer.AutopostFilter
import dev.inmo.plaguposter.triggers.timer.TimersRepo
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module
object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) {
singleWithRandomQualifier<AutopostFilter> {
val timersRepo = get<TimersRepo>()
AutopostFilter { _, dateTime ->
val result = timersRepo.keys(dateTime, FirstPagePagination(1))
result.results.isEmpty()
}
}
}
}

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.plaguposter.triggers.timer.disablers.autoposts"/>

View File

@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":plaguposter.common")
api project(":plaguposter.triggers.timer")
api project(":plaguposter.ratings")
}
}
}
}

View File

@ -0,0 +1 @@
package dev.inmo.plaguposter.triggers.timer.disablers.ratings

View File

@ -0,0 +1,26 @@
package dev.inmo.plaguposter.triggers.timer.disablers.ratings
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithRandomQualifier
import dev.inmo.micro_utils.repos.unset
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.plaguposter.triggers.timer.TimersRepo
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module
object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) {
singleWithRandomQualifier(createdAtStart = true) {
val timersRepo = get<TimersRepo>()
val ratingsRepo = get<RatingsRepo>()
val scope = get<CoroutineScope>()
timersRepo.onNewValue.subscribeSafelyWithoutExceptions(scope) {
ratingsRepo.unset(it.first)
}
}
}
}

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.plaguposter.triggers.timer.disablers.ratings"/>

View File

@ -0,0 +1,287 @@
package dev.inmo.plaguposter.triggers.timer
import com.soywiz.klock.DateFormat
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.micro_utils.repos.unset
import dev.inmo.plaguposter.common.SuccessfulSymbol
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.delete
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
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.utils.bold
import dev.inmo.tgbotapi.utils.buildEntities
import dev.inmo.tgbotapi.utils.row
object ButtonsBuilder {
private const val changeTimeData = "timer_time_hint"
private const val changeDateData = "timer_date_hint"
private const val changeHoursDataPrefix = "timer_h"
private const val changeMinutesDataPrefix = "timer_m"
private const val changeDayDataPrefix = "timer_d"
private const val changeMonthDataPrefix = "timer_M"
private const val changeYearDataPrefix = "timer_y"
private const val changeDateDataPrefix = "timer_s"
private const val cancelDateData = "timer_c"
private const val deleteDateDataPrefix = "timer_r"
val datePrintFormat = DateFormat("HH:mm, dd.MM.yyyy, zzz")
fun buildTimerButtons(
postId: PostId,
dateTime: DateTimeTz,
exists: Boolean
) = inlineKeyboard {
val unixMillis = dateTime.utc.unixMillisLong
row {
dataButton("Time (hh:mm):", changeTimeData)
dataButton(dateTime.hours.toString(), "$changeHoursDataPrefix $postId $unixMillis")
dataButton(dateTime.minutes.toString(), "$changeMinutesDataPrefix $postId $unixMillis")
}
row {
dataButton("Date (dd.mm.yyyy):", changeDateData)
dataButton("${dateTime.dayOfMonth}", "$changeDayDataPrefix $postId $unixMillis")
dataButton("${dateTime.month1}", "$changeMonthDataPrefix $postId $unixMillis")
dataButton("${dateTime.yearInt}", "$changeYearDataPrefix $postId $unixMillis")
}
row {
if (exists) {
dataButton("\uD83D\uDDD1", "$deleteDateDataPrefix $postId")
}
dataButton(UnsuccessfulSymbol, cancelDateData)
dataButton(SuccessfulSymbol, "$changeDateDataPrefix $postId $unixMillis")
}
}
fun buildTimerTextSources(
currentDateTime: DateTime,
previousTime: DateTime?
) = buildEntities {
previousTime ?.let {
+ "Previous timer time: " + bold(it.local.toString(datePrintFormat)) + "\n"
}
+"Currently editing time: " + bold(currentDateTime.local.toString(datePrintFormat))
}
suspend fun BehaviourContext.includeKeyboardHandling(
timersRepo: TimersRepo,
onSavePublishingTime: suspend (PostId, DateTime) -> Boolean
) {
fun buildKeyboard(
prefix: String,
postId: PostId,
values: Iterable<Int>,
min: DateTime = nearestAvailableTimerTime(),
dateConverter: (Int) -> DateTimeTz
): InlineKeyboardMarkup {
return inlineKeyboard {
values.chunked(6).forEach {
row {
it.forEach {
dataButton(it.toString(), "$prefix $postId ${dateConverter(it).utc.unixMillisLong.coerceAtLeast(min.unixMillisLong)}")
}
}
}
}
}
suspend fun buildStandardDataCallbackQuery(
name: String,
prefix: String,
possibleValues: (DateTimeTz) -> Iterable<Int>,
dateTimeConverter: (Int, DateTimeTz) -> DateTimeTz
) {
val setPrefix = "${prefix}s"
onMessageDataCallbackQuery(Regex("$prefix .+")) {
val (_, rawPostId, rawDateTimeMillis) = it.data.split(" ")
val currentMillis = rawDateTimeMillis.toLongOrNull() ?: return@onMessageDataCallbackQuery
val currentDateTime = DateTime(currentMillis)
val currentDateTimeLocal = DateTime(currentMillis).local
val postId = PostId(rawPostId)
val previousTime = timersRepo.get(postId)
edit (
it.message.withContentOrNull() ?: return@onMessageDataCallbackQuery,
replyMarkup = buildKeyboard(
setPrefix,
postId,
possibleValues(currentDateTimeLocal)
) {
dateTimeConverter(it, currentDateTimeLocal)
}
) {
+buildTimerTextSources(currentDateTime, previousTime) + "\n"
+"You are about to edit $name"
}
}
onMessageDataCallbackQuery(Regex("$setPrefix .+")) {
val (_, rawPostId, rawDateTimeMillis) = it.data.split(" ")
val currentMillis = rawDateTimeMillis.toLongOrNull() ?: return@onMessageDataCallbackQuery
val currentDateTime = DateTime(currentMillis)
val postId = PostId(rawPostId)
val previousTime = timersRepo.get(postId)
edit(
it.message.withContentOrNull() ?: return@onMessageDataCallbackQuery,
replyMarkup = buildTimerButtons(
postId,
currentDateTime.local,
timersRepo.contains(postId)
)
) {
+buildTimerTextSources(currentDateTime, previousTime)
}
}
}
fun DateTimeTz.dateEq(other: DateTimeTz) = yearInt == other.yearInt && month0 == other.month0 && dayOfMonth == other.dayOfMonth
buildStandardDataCallbackQuery(
"hour",
changeHoursDataPrefix,
{
val now = nearestAvailableTimerTime().local
if (now.dateEq(it)) {
now.hours .. 23
} else {
0 .. 23
}
}
) { newValue, oldDateTime ->
DateTimeTz.local(
oldDateTime.local.copyDayOfMonth(hours = newValue),
oldDateTime.offset
)
}
buildStandardDataCallbackQuery(
"minute",
changeMinutesDataPrefix,
{
val now = nearestAvailableTimerTime().local
if (now.dateEq(it) && now.hours >= it.hours) {
now.minutes until 60
} else {
0 until 60
}
}
) { newValue, oldDateTime ->
DateTimeTz.local(
oldDateTime.local.copyDayOfMonth(minutes = newValue),
oldDateTime.offset
)
}
buildStandardDataCallbackQuery(
"day",
changeDayDataPrefix,
{
val now = nearestAvailableTimerTime().local
if (now.yearInt == it.yearInt && now.month0 == it.month0) {
now.dayOfMonth .. it.month.days(it.year)
} else {
1 .. it.month.days(it.year)
}
}
) { newValue, oldDateTime ->
DateTimeTz.local(
oldDateTime.local.copyDayOfMonth(dayOfMonth = newValue),
oldDateTime.offset
)
}
buildStandardDataCallbackQuery(
"month",
changeMonthDataPrefix,
{
val now = nearestAvailableTimerTime().local
if (now.year == it.year) {
now.month1 .. 12
} else {
1 .. 12
}
}
) { newValue, oldDateTime ->
DateTimeTz.local(
oldDateTime.local.copyDayOfMonth(month = Month(newValue)),
oldDateTime.offset
)
}
buildStandardDataCallbackQuery(
"year",
changeYearDataPrefix,
{
val now = nearestAvailableTimerTime().local
(now.year.year .. (now.year.year + 5))
}
) { newValue, oldDateTime ->
DateTimeTz.local(
oldDateTime.local.copyDayOfMonth(year = Year(newValue)),
oldDateTime.offset
)
}
onMessageDataCallbackQuery(changeTimeData) {
answer(it, "Use the buttons to the right to set post publishing time (hh:mm)", showAlert = true)
}
onMessageDataCallbackQuery(changeDateData) {
answer(it, "Use the buttons to the right to set post publishing date (dd.MM.yyyy)", showAlert = true)
}
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"
)
it.message.delete(this)
}
onMessageDataCallbackQuery(Regex("$deleteDateDataPrefix .*")) {
val (_, rawPostId) = it.data.split(" ")
val postId = PostId(rawPostId)
val success = runCatchingSafely {
timersRepo.unset(postId)
true
}.getOrElse { false }
answer(
it,
if (success) "Successfully unset timer" else "Unable to unset timer"
)
it.message.delete(this)
}
onMessageDataCallbackQuery(cancelDateData) {
delete(it.message)
}
}
}

View File

@ -0,0 +1,9 @@
package dev.inmo.plaguposter.triggers.timer
import com.soywiz.klock.DateTime
import com.soywiz.klock.minutes
fun nearestAvailableTimerTime() = (DateTime.now() + 1.minutes).copyDayOfMonth(
milliseconds = 0,
seconds = 0
)

View File

@ -0,0 +1 @@
package dev.inmo.plaguposter.triggers.timer

View File

@ -0,0 +1,28 @@
package dev.inmo.plaguposter.triggers.timer
import dev.inmo.plaguposter.common.SuccessfulSymbol
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.posts.models.RegisteredPost
import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.InlineKeyboardButton
class TimerPanelButton(
private val timersRepo: TimersRepo
) : PanelButtonBuilder {
override val weight: Int
get() = 0
override suspend fun buildButton(post: RegisteredPost): InlineKeyboardButton? {
val publishingTime = timersRepo.get(post.id)
return CallbackDataInlineKeyboardButton(
"${ if (publishingTime == null) UnsuccessfulSymbol else SuccessfulSymbol }",
"$timerSetPrefix ${post.id}"
)
}
companion object {
const val timerSetPrefix = "timer_set_init"
}
}

View File

@ -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<PostId, Job>? = 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()
}
}
}
}
}

View File

@ -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<PostId, DateTime> {
suspend fun getMinimalDateTimePost(): Pair<PostId, DateTime>?
}

View File

@ -0,0 +1,80 @@
package dev.inmo.plaguposter.triggers.timer
import com.soywiz.klock.DateTime
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithRandomQualifierAndBinds
import dev.inmo.micro_utils.repos.set
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI
import dev.inmo.plaguposter.posts.repo.ReadPostsRepo
import dev.inmo.plaguposter.triggers.timer.repo.ExposedTimersRepo
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import kotlinx.coroutines.CoroutineScope
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()) }
singleWithRandomQualifierAndBinds { TimerPanelButton(get()) }
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val timersRepo = koin.get<TimersRepo>()
val chatsConfig = koin.get<ChatConfig>()
val panelApi = koin.get<PanelButtonsAPI>()
val scope = koin.get<CoroutineScope>()
with(ButtonsBuilder) {
includeKeyboardHandling(timersRepo) { postId, dateTime ->
timersRepo.set(postId, dateTime)
true
}
}
timersRepo.onNewValue.subscribeSafelyWithoutExceptions(scope) {
panelApi.forceRefresh(it.first)
}
timersRepo.onValueRemoved.subscribeSafelyWithoutExceptions(scope) {
panelApi.forceRefresh(it)
}
onMessageDataCallbackQuery(
Regex("${TimerPanelButton.timerSetPrefix} [^\\s]+"),
initialFilter = {
chatsConfig.check(it.message.chat.id)
}
) {
val (_, postIdRaw) = it.data.split(" ")
val postId = PostId(postIdRaw)
val now = nearestAvailableTimerTime()
val exists = timersRepo.get(postId)
val textSources = ButtonsBuilder.buildTimerTextSources(now, exists)
val buttons = ButtonsBuilder.buildTimerButtons(
postId,
now.local,
exists != null
)
reply(
it.message,
textSources,
replyMarkup = buttons
)
answer(it)
}
}
}

View File

@ -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<PostId, DateTime>(
database,
"timers"
) {
override val keyColumn = text("post_id")
private val dateTimeColumn = long("date_time")
override val selectById: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { keyColumn.eq(it.string) }
override val selectByValue: ISqlExpressionBuilder.(DateTime) -> Op<Boolean> = { 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<Int>) {
it[dateTimeColumn] = v.unixMillisLong
}
override fun insertKey(k: PostId, v: DateTime, it: InsertStatement<Number>) {
it[keyColumn] = k.string
}
override suspend fun getMinimalDateTimePost(): Pair<PostId, DateTime>? = transaction(database) {
selectAll().orderBy(dateTimeColumn).limit(1).firstOrNull() ?.let {
PostId(it[keyColumn]) to DateTime(it[dateTimeColumn])
}
}
}

View File

@ -0,0 +1 @@
<manifest package="dev.inmo.plaguposter.triggers.timer"/>