mirror of
https://github.com/InsanusMokrassar/PlaguPoster.git
synced 2025-12-05 20:45:40 +00:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ad967c002a | |||
| e3958aff3f | |||
| ae6993efba | |||
| 991b0ef3d3 | |||
| aa6c62d66e | |||
| e6a0d67444 | |||
| 16bd62da51 | |||
| edc75a09ee | |||
| 0f958f94cb | |||
| 69320a3b62 | |||
| b6fa2a6cd3 | |||
| e24237beb3 | |||
| d2d3665c5e | |||
| 9ef9de2b8a | |||
| cef350667b | |||
| 80ed679241 | |||
| 39391636ac | |||
| 29a19df7fc | |||
| 250f88e2fe | |||
| bb433a6441 | |||
| 7a8166153f | |||
| 114add0391 | |||
| 58b1f26502 | |||
| ba3d054f0f | |||
| eef2bfce14 | |||
| fe96101631 | |||
| 7abb6efba3 | |||
| 2f0a823f7c | |||
| 730e3c50e9 | |||
| 0cc0510876 | |||
| 947bd7c2c4 | |||
| 7f54e86962 | |||
| db419165a7 | |||
| a5b0f429a0 | |||
| 9c161b6dab | |||
| f6067bb096 | |||
| 248740f246 | |||
| 3ae3cabd80 | |||
| 5fd4042fe3 | |||
| 6df4546b81 | |||
| 12635c654a | |||
| 15bd013eaa | |||
| 39b607c4e7 | |||
| 98f3e2a461 | |||
| 0d31d90efd | |||
| 0ce202a5f6 | |||
| 077f8c30a6 | |||
| 1e9559a2c9 | |||
| feef8efee1 | |||
| 54eb7515d3 | |||
| 1bb12bee0e | |||
| 467525e48d | |||
| 29e5a04135 | |||
| 6eb43055a7 | |||
| 57eebb61d5 | |||
| 87957dba30 | |||
| 20148c02f0 | |||
| e17cfa1c7c | |||
| 0a5ffee808 | |||
| 847b285ce3 | |||
| c449457d86 | |||
| 1b3a632d7b | |||
| ebfa79cf64 | |||
| e59c7b0f7e | |||
| 7a4fb05bfb | |||
| 7bc7bf6e8c | |||
| c64faf75d0 | |||
| ee80d8a3a1 |
5
.github/workflows/build_and_publish.yml
vendored
5
.github/workflows/build_and_publish.yml
vendored
@@ -8,12 +8,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
java-version: 17
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
printf "\norg.gradle.jvmargs=-Xmx1g -Xms500m\nkotlin.daemon.jvmargs=-Xmx1g -Xms500m\norg.gradle.daemon=false" >> gradle.properties
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
|
||||
rm gradle.properties
|
||||
|
||||
4
.github/workflows/docker-publish.yml
vendored
4
.github/workflows/docker-publish.yml
vendored
@@ -11,8 +11,12 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 17
|
||||
- name: Rewrite version
|
||||
run: |
|
||||
printf "\norg.gradle.jvmargs=-Xmx1g -Xms500m\nkotlin.daemon.jvmargs=-Xmx1g -Xms500m\norg.gradle.daemon=false" >> gradle.properties
|
||||
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
|
||||
if [[ "$branch" != "master" ]]; then
|
||||
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
|
||||
|
||||
90
CHANGELOG.md
90
CHANGELOG.md
@@ -1,8 +1,96 @@
|
||||
# PlaguPoster
|
||||
|
||||
## 0.6.0
|
||||
|
||||
* Dependencies update
|
||||
|
||||
## 0.5.6
|
||||
|
||||
* `Ratings`:
|
||||
* `GC`:
|
||||
* Now GC will autoclear ratings even if post is absent in posts repo
|
||||
|
||||
## 0.5.5
|
||||
|
||||
* Dependencies update
|
||||
|
||||
## 0.5.4
|
||||
|
||||
* Dependencies update
|
||||
|
||||
## 0.5.3
|
||||
|
||||
* Dependencies update
|
||||
|
||||
## 0.5.2
|
||||
|
||||
* Dependencies update
|
||||
|
||||
## 0.5.1
|
||||
|
||||
* Add opportunity to set unique
|
||||
|
||||
## 0.5.0
|
||||
|
||||
* Dependencies update
|
||||
* Since this update bots will require **`JDK` 17+**
|
||||
|
||||
## 0.3.0
|
||||
|
||||
* `Versions`:
|
||||
* `tgbotapi`: `9.1.0`
|
||||
* `plagubot`: `7.1.0`
|
||||
* `plagubot-plugins`: `0.14.0`
|
||||
|
||||
## 0.2.3
|
||||
|
||||
* Add opportunity to use several target chat ids
|
||||
* Update dependencies
|
||||
|
||||
## 0.2.2
|
||||
|
||||
* `GarbageCollector`:
|
||||
* Now on start will all clearing job done
|
||||
|
||||
## 0.2.1
|
||||
|
||||
* `Versions`:
|
||||
* `kotlin`: `1.8.21`
|
||||
* `tgbotapi`: `7.1.2`
|
||||
* `plagubot`: `5.1.2`
|
||||
* `microutils`: `0.18.1`
|
||||
* `kslog`: `1.1.1`
|
||||
* `plagubot.plugins`: `0.11.2`
|
||||
* `psql`: `42.6.0`
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* `Versions`:
|
||||
* `tgbotapi`: `7.1.0`
|
||||
* `plagubot`: `5.1.0`
|
||||
* `krontab`: `1.0.0`
|
||||
* `plagubot.plugins`: `0.11.0`
|
||||
|
||||
## 0.1.2
|
||||
|
||||
* `Versions`:
|
||||
* `kotlin`: `1.8.20`
|
||||
* `plagubot`: `5.0.2`
|
||||
* `microutils`: `0.17.8`
|
||||
* `kslog`: `1.1.1`
|
||||
* `plagubot.plugins`: `0.10.2`
|
||||
* `psql`: `42.6.0`
|
||||
|
||||
## 0.1.1
|
||||
|
||||
* Update dependencies
|
||||
* `Triggers`
|
||||
* `SelectorWithTimer`
|
||||
* Opportunity to get schedule of posts using `publishing_autoschedule` command
|
||||
|
||||
## 0.0.10
|
||||
|
||||
## 0.0.9
|
||||
|
||||
* Update depedencies
|
||||
* Update dependencies
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ allprojects {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
google()
|
||||
maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" }
|
||||
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package dev.inmo.plaguposter.common
|
||||
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -10,17 +9,34 @@ import kotlinx.serialization.Serializable
|
||||
data class ChatConfig(
|
||||
@SerialName("targetChat")
|
||||
@Serializable(FullChatIdentifierSerializer::class)
|
||||
val targetChatId: IdChatIdentifier,
|
||||
val targetChatId: IdChatIdentifier? = null,
|
||||
@SerialName("sourceChat")
|
||||
@Serializable(FullChatIdentifierSerializer::class)
|
||||
val sourceChatId: IdChatIdentifier,
|
||||
val sourceChatId: IdChatIdentifier?,
|
||||
@SerialName("cacheChat")
|
||||
@Serializable(FullChatIdentifierSerializer::class)
|
||||
val cacheChatId: IdChatIdentifier
|
||||
val cacheChatId: IdChatIdentifier,
|
||||
@SerialName("targetChats")
|
||||
val targetChatIds: List<@Serializable(FullChatIdentifierSerializer::class) IdChatIdentifier> = emptyList(),
|
||||
@SerialName("sourceChats")
|
||||
val sourceChatIds: List<@Serializable(FullChatIdentifierSerializer::class) IdChatIdentifier> = emptyList(),
|
||||
) {
|
||||
val allTargetChatIds by lazy {
|
||||
(listOfNotNull(targetChatId) + targetChatIds).toSet()
|
||||
}
|
||||
val allSourceChatIds by lazy {
|
||||
(listOfNotNull(sourceChatId) + sourceChatIds).toSet()
|
||||
}
|
||||
|
||||
init {
|
||||
require(targetChatId != null || targetChatIds.isNotEmpty()) {
|
||||
"One of fields, 'targetChat' or 'targetChats' should be presented"
|
||||
}
|
||||
}
|
||||
|
||||
fun check(chatId: IdChatIdentifier) = when (chatId) {
|
||||
targetChatId,
|
||||
sourceChatId,
|
||||
in allTargetChatIds,
|
||||
in allSourceChatIds,
|
||||
cacheChatId -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@ package dev.inmo.plaguposter.common
|
||||
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.CommonMessageFilterExcludeMediaGroups
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.SimpleFilter
|
||||
import dev.inmo.tgbotapi.extensions.utils.contentMessageOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
|
||||
import dev.inmo.tgbotapi.types.BotCommand
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.*
|
||||
import dev.inmo.tgbotapi.types.message.content.TextContent
|
||||
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
|
||||
|
||||
val FirstSourceIsCommandsFilter = SimpleFilter<Message> {
|
||||
it is ContentMessage<*> && it.content.textContentOrNull() ?.textSources ?.firstOrNull {
|
||||
it is BotCommandTextSource
|
||||
} != null
|
||||
it.contentMessageOrNull() ?.withContentOrNull<TextContent>() ?.content ?.textSources ?.firstOrNull() is BotCommandTextSource
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.plaguposter.common
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package dev.inmo.plaguposter.common
|
||||
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageIdentifier
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.Message
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -11,7 +8,7 @@ import kotlinx.serialization.Serializable
|
||||
data class ShortMessageInfo(
|
||||
@Serializable(FullChatIdentifierSerializer::class)
|
||||
val chatId: IdChatIdentifier,
|
||||
val messageId: MessageIdentifier
|
||||
val messageId: MessageId
|
||||
)
|
||||
|
||||
fun Message.short() = ShortMessageInfo(chat.id, messageId)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package dev.inmo.plaguposter.common
|
||||
|
||||
import dev.inmo.kslog.common.i
|
||||
import dev.inmo.kslog.common.iS
|
||||
import dev.inmo.kslog.common.logger
|
||||
import dev.inmo.plagubot.Plugin
|
||||
@@ -27,8 +26,8 @@ object CommonPlugin : Plugin {
|
||||
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
|
||||
val config = koin.get<ChatConfig>()
|
||||
|
||||
Log.iS { "Target chat info: ${getChat(config.targetChatId)}" }
|
||||
Log.iS { "Source chat info: ${getChat(config.sourceChatId)}" }
|
||||
Log.iS { "Target chats info: ${config.allTargetChatIds.map { getChat(it) }.joinToString()}" }
|
||||
Log.iS { "Source chats info: ${config.allSourceChatIds.map { getChat(it) }.joinToString()}" }
|
||||
Log.iS { "Cache chat info: ${getChat(config.cacheChatId)}" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
kotlin.code.style=official
|
||||
org.gradle.jvmargs=-Xmx1024m
|
||||
org.gradle.parallel=true
|
||||
kotlin.js.generate.externals=true
|
||||
kotlin.incremental=true
|
||||
@@ -10,4 +9,4 @@ android.enableJetifier=true
|
||||
# Project data
|
||||
|
||||
group=dev.inmo
|
||||
version=0.1.0
|
||||
version=0.6.0
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
[versions]
|
||||
|
||||
kotlin = "1.8.10"
|
||||
kotlin-serialization = "1.5.0"
|
||||
kotlin = "1.9.23"
|
||||
kotlin-serialization = "1.6.3"
|
||||
|
||||
plagubot = "5.0.0"
|
||||
tgbotapi = "7.0.0"
|
||||
microutils = "0.17.5"
|
||||
kslog = "1.0.0"
|
||||
krontab = "0.9.0"
|
||||
tgbotapi-libraries = "0.10.0"
|
||||
plagubot-plugins = "0.10.0"
|
||||
plagubot = "8.4.0"
|
||||
tgbotapi = "15.0.0"
|
||||
microutils = "0.21.1"
|
||||
kslog = "1.3.4"
|
||||
krontab = "2.3.0"
|
||||
plagubot-plugins = "0.19.0"
|
||||
|
||||
dokka = "1.8.10"
|
||||
dokka = "1.9.20"
|
||||
|
||||
psql = "42.5.0"
|
||||
psql = "42.6.0"
|
||||
|
||||
[libraries]
|
||||
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -7,7 +7,7 @@ kotlin {
|
||||
jvm {
|
||||
compilations.main {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,6 @@ kotlin {
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@ project.group = "$group"
|
||||
apply from: "$publishGradlePath"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
jvm {
|
||||
compilations.main {
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
}
|
||||
js (IR) {
|
||||
browser()
|
||||
nodejs()
|
||||
@@ -38,3 +44,8 @@ kotlin {
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,14 @@ kotlin {
|
||||
commonMain {
|
||||
dependencies {
|
||||
api project(":plaguposter.common")
|
||||
api project(":plaguposter.posts")
|
||||
api libs.microutils.koin
|
||||
api libs.krontab
|
||||
}
|
||||
}
|
||||
jvmMain {
|
||||
dependencies {
|
||||
api libs.plagubot.plugins.inline.queries
|
||||
}
|
||||
}
|
||||
}
|
||||
1
posts/gc/src/commonMain/kotlin/PackageInfo.kt
Normal file
1
posts/gc/src/commonMain/kotlin/PackageInfo.kt
Normal file
@@ -0,0 +1 @@
|
||||
package dev.inmo.plaguposter.posts.gc
|
||||
194
posts/gc/src/jvmMain/kotlin/Plugin.kt
Normal file
194
posts/gc/src/jvmMain/kotlin/Plugin.kt
Normal file
@@ -0,0 +1,194 @@
|
||||
package dev.inmo.plaguposter.posts.gc
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import dev.inmo.krontab.KrontabTemplate
|
||||
import dev.inmo.krontab.toKronScheduler
|
||||
import dev.inmo.krontab.utils.asFlowWithDelays
|
||||
import dev.inmo.kslog.common.KSLog
|
||||
import dev.inmo.kslog.common.i
|
||||
import dev.inmo.kslog.common.iS
|
||||
import dev.inmo.micro_utils.coroutines.actor
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.repos.deleteById
|
||||
import dev.inmo.plagubot.Plugin
|
||||
import dev.inmo.plagubot.plugins.inline.queries.models.Format
|
||||
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
|
||||
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
|
||||
import dev.inmo.plaguposter.common.ChatConfig
|
||||
import dev.inmo.plaguposter.posts.models.NewPost
|
||||
import dev.inmo.plaguposter.posts.models.PostContentInfo
|
||||
import dev.inmo.plaguposter.posts.repo.PostsRepo
|
||||
import dev.inmo.tgbotapi.extensions.api.delete
|
||||
import dev.inmo.tgbotapi.extensions.api.edit.edit
|
||||
import dev.inmo.tgbotapi.extensions.api.forwardMessage
|
||||
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitInlineMessageIdDataCallbackQuery
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.oneOf
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.parallel
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
|
||||
import dev.inmo.tgbotapi.types.MilliSeconds
|
||||
import dev.inmo.tgbotapi.utils.bold
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.koin.core.Koin
|
||||
import org.koin.core.module.Module
|
||||
|
||||
object Plugin : Plugin {
|
||||
@Serializable
|
||||
internal data class Config (
|
||||
val krontab: KrontabTemplate? = null,
|
||||
val throttlingMillis: MilliSeconds = 1000,
|
||||
val doFullCheck: Boolean = false
|
||||
)
|
||||
override fun Module.setupDI(database: Database, params: JsonObject) {
|
||||
params["messagesChecker"] ?.let { element ->
|
||||
single { get<Json>().decodeFromJsonElement(Config.serializer(), element) }
|
||||
}
|
||||
}
|
||||
|
||||
private val gcLogger = KSLog("GarbageCollector")
|
||||
private suspend fun BehaviourContext.doRecheck(
|
||||
throttlingMillis: MilliSeconds,
|
||||
doFullCheck: Boolean,
|
||||
postsRepo: PostsRepo,
|
||||
chatsConfig: ChatConfig
|
||||
) {
|
||||
val posts = postsRepo.getAll()
|
||||
gcLogger.i {
|
||||
"Start garbage collecting of posts. Initial posts count: ${posts.size}"
|
||||
}
|
||||
posts.forEach { (postId, post) ->
|
||||
val surelyAbsentMessages = mutableListOf<PostContentInfo>()
|
||||
for (content in post.content) {
|
||||
try {
|
||||
forwardMessage(
|
||||
toChatId = chatsConfig.cacheChatId,
|
||||
fromChatId = content.chatId,
|
||||
messageId = content.messageId
|
||||
)
|
||||
|
||||
if (!doFullCheck) {
|
||||
break
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e.message ?.contains("message to forward not found") == true) {
|
||||
surelyAbsentMessages.add(content)
|
||||
}
|
||||
}
|
||||
delay(throttlingMillis)
|
||||
}
|
||||
val existsPostMessages = post.content.filter {
|
||||
it !in surelyAbsentMessages
|
||||
}
|
||||
if (existsPostMessages.isNotEmpty() && surelyAbsentMessages.isNotEmpty()) {
|
||||
runCatching {
|
||||
postsRepo.update(
|
||||
postId,
|
||||
NewPost(
|
||||
content = existsPostMessages
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (existsPostMessages.isNotEmpty()) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
runCatching {
|
||||
send(
|
||||
chatsConfig.cacheChatId,
|
||||
"Can't find any messages for post $postId. So, deleting it"
|
||||
)
|
||||
}
|
||||
runCatching {
|
||||
postsRepo.deleteById(postId)
|
||||
}
|
||||
}
|
||||
gcLogger.iS {
|
||||
"Complete garbage collecting of posts. Result posts count: ${postsRepo.count()}"
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
|
||||
val postsRepo = koin.get<PostsRepo>()
|
||||
val chatsConfig = koin.get<ChatConfig>()
|
||||
val config = koin.getOrNull<Config>() ?: Config()
|
||||
|
||||
val scope = koin.get<CoroutineScope>()
|
||||
|
||||
val recheckActor = scope.actor<Unit>(0) {
|
||||
runCatching {
|
||||
doRecheck(
|
||||
config.throttlingMillis,
|
||||
config.doFullCheck,
|
||||
postsRepo,
|
||||
chatsConfig
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
config.krontab ?.toKronScheduler() ?.asFlowWithDelays() ?.subscribeSafelyWithoutExceptions(koin.get()) {
|
||||
recheckActor.trySend(Unit)
|
||||
}
|
||||
|
||||
onCommand("force_garbage_collection") { message ->
|
||||
launch {
|
||||
val prefix = uuid4().toString()
|
||||
val yesData = "${prefix}yes"
|
||||
val noData = "${prefix}no"
|
||||
edit(
|
||||
message,
|
||||
text = "Are you sure want to trigger posts garbage collecting?",
|
||||
replyMarkup = flatInlineKeyboard {
|
||||
dataButton("Sure", yesData)
|
||||
dataButton("No", noData)
|
||||
}
|
||||
)
|
||||
|
||||
val answer = oneOf(
|
||||
parallel {
|
||||
waitMessageDataCallbackQuery().filter {
|
||||
it.message.sameMessage(message)
|
||||
}.first()
|
||||
},
|
||||
parallel {
|
||||
waitInlineMessageIdDataCallbackQuery().filter {
|
||||
it.data == yesData || it.data == noData
|
||||
}.first()
|
||||
}
|
||||
)
|
||||
|
||||
if (answer.data == yesData) {
|
||||
if (recheckActor.trySend(Unit).isSuccess) {
|
||||
edit(message, "Checking of posts without exists messages triggered")
|
||||
} else {
|
||||
edit(message) {
|
||||
+"Checking of posts without exists messages has been triggered " + bold("earlier")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
delete(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
koin.getOrNull<InlineTemplatesRepo>() ?.addTemplate(
|
||||
OfferTemplate(
|
||||
"Force posts check",
|
||||
listOf(
|
||||
Format("/force_garbage_collection")
|
||||
),
|
||||
"Force check posts without exists messages"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,11 @@ import com.benasher44.uuid.uuid4
|
||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.koin.getAllDistinct
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
|
||||
import dev.inmo.micro_utils.repos.cache.cached
|
||||
import dev.inmo.micro_utils.repos.cache.full.cached
|
||||
import dev.inmo.micro_utils.repos.deleteById
|
||||
import dev.inmo.micro_utils.repos.id
|
||||
import dev.inmo.micro_utils.repos.set
|
||||
import dev.inmo.micro_utils.repos.unset
|
||||
import dev.inmo.micro_utils.repos.value
|
||||
import dev.inmo.micro_utils.repos.cache.full.fullyCached
|
||||
import dev.inmo.plagubot.Plugin
|
||||
import dev.inmo.plaguposter.common.ChatConfig
|
||||
import dev.inmo.plaguposter.common.UnsuccessfulSymbol
|
||||
@@ -33,7 +30,8 @@ import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageId
|
||||
import dev.inmo.tgbotapi.types.ReplyParameters
|
||||
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
|
||||
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
|
||||
import dev.inmo.tgbotapi.types.message.ParseMode
|
||||
@@ -99,7 +97,7 @@ object Plugin : Plugin {
|
||||
val api = koin.get<PanelButtonsAPI>()
|
||||
val basePostsMessages = PostsMessages(koin.get(), koin.get())
|
||||
val postsMessages = if (koin.useCache) {
|
||||
basePostsMessages.cached(FullKVCache(), koin.get())
|
||||
basePostsMessages.fullyCached(MapKeyValueRepo(), koin.get())
|
||||
} else {
|
||||
basePostsMessages
|
||||
}
|
||||
@@ -115,7 +113,7 @@ object Plugin : Plugin {
|
||||
firstContent.chatId,
|
||||
text = config.text,
|
||||
parseMode = config.parseMode,
|
||||
replyToMessageId = firstContent.messageId,
|
||||
replyParameters = ReplyParameters(firstContent.chatId, firstContent.messageId),
|
||||
replyMarkup = InlineKeyboardMarkup(buttons),
|
||||
disableNotification = true
|
||||
).also { sentMessage ->
|
||||
@@ -131,7 +129,7 @@ object Plugin : Plugin {
|
||||
suspend fun refreshPostMessage(
|
||||
postId: PostId,
|
||||
chatId: IdChatIdentifier,
|
||||
messageId: MessageIdentifier
|
||||
messageId: MessageId
|
||||
) {
|
||||
val post = postsRepo.getById(postId) ?: return
|
||||
val buttons = api.buttonsBuilders.chunked(config.buttonsPerRow).mapNotNull { row ->
|
||||
@@ -149,7 +147,7 @@ object Plugin : Plugin {
|
||||
|
||||
onMessageDataCallbackQuery (
|
||||
initialFilter = {
|
||||
it.data.startsWith(PanelButtonsAPI.openGlobalMenuDataPrefix) && it.message.chat.id == chatsConfig.sourceChatId
|
||||
it.data.startsWith(PanelButtonsAPI.openGlobalMenuDataPrefix) && it.message.chat.id in chatsConfig.allSourceChatIds
|
||||
}
|
||||
) {
|
||||
val postId = it.data.removePrefix(PanelButtonsAPI.openGlobalMenuDataPrefix).let(::PostId)
|
||||
@@ -158,7 +156,7 @@ object Plugin : Plugin {
|
||||
}
|
||||
onMessageDataCallbackQuery(
|
||||
initialFilter = {
|
||||
it.data.startsWith("delete ") && it.message.chat.id == chatsConfig.sourceChatId
|
||||
it.data.startsWith("delete ") && it.message.chat.id in chatsConfig.allSourceChatIds
|
||||
}
|
||||
) { query ->
|
||||
val postId = query.data.removePrefix("delete ").let(::PostId)
|
||||
@@ -185,7 +183,7 @@ object Plugin : Plugin {
|
||||
}
|
||||
onMessageDataCallbackQuery(
|
||||
initialFilter = {
|
||||
it.data.startsWith("refresh ") && it.message.chat.id == chatsConfig.sourceChatId
|
||||
it.data.startsWith("refresh ") && it.message.chat.id in chatsConfig.allSourceChatIds
|
||||
}
|
||||
) { query ->
|
||||
val postId = query.data.removePrefix("refresh ").let(::PostId)
|
||||
|
||||
@@ -4,21 +4,18 @@ import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageIdentifier
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import kotlinx.serialization.builtins.PairSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
|
||||
private val ChatIdToMessageSerializer = PairSerializer(FullChatIdentifierSerializer, MessageIdentifier.serializer())
|
||||
private val ChatIdToMessageSerializer = PairSerializer(FullChatIdentifierSerializer, MessageId.serializer())
|
||||
|
||||
fun PostsMessages(
|
||||
database: Database,
|
||||
json: Json
|
||||
): KeyValueRepo<PostId, Pair<IdChatIdentifier, MessageIdentifier>> = ExposedKeyValueRepo<String, String>(
|
||||
): KeyValueRepo<PostId, Pair<IdChatIdentifier, MessageId>> = ExposedKeyValueRepo<String, String>(
|
||||
database,
|
||||
{ text("postId") },
|
||||
{ text("chatToMessage") },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.plaguposter.posts.models
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.plaguposter.common.DateTimeSerializer
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package dev.inmo.plaguposter.posts.models
|
||||
|
||||
import dev.inmo.tgbotapi.extensions.utils.possiblyMediaGroupMessageOrNull
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageIdentifier
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -13,8 +10,8 @@ import kotlinx.serialization.Serializable
|
||||
data class PostContentInfo(
|
||||
@Serializable(FullChatIdentifierSerializer::class)
|
||||
val chatId: IdChatIdentifier,
|
||||
val messageId: MessageIdentifier,
|
||||
val group: String?,
|
||||
val messageId: MessageId,
|
||||
val group: MediaGroupId?,
|
||||
val order: Int
|
||||
) {
|
||||
companion object {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package dev.inmo.plaguposter.posts.repo
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.micro_utils.repos.ReadCRUDRepo
|
||||
import dev.inmo.plaguposter.posts.models.*
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageId
|
||||
|
||||
interface ReadPostsRepo : ReadCRUDRepo<RegisteredPost, PostId> {
|
||||
suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageIdentifier): PostId?
|
||||
suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageId): PostId?
|
||||
suspend fun getPostCreationTime(postId: PostId): DateTime?
|
||||
suspend fun getFirstMessageInfo(postId: PostId): PostContentInfo?
|
||||
}
|
||||
|
||||
@@ -11,20 +11,19 @@ import dev.inmo.tgbotapi.extensions.api.send.copyMessage
|
||||
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||
import dev.inmo.tgbotapi.extensions.utils.*
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
|
||||
import dev.inmo.tgbotapi.types.message.content.MediaGroupPartContent
|
||||
|
||||
class PostPublisher(
|
||||
private val bot: TelegramBot,
|
||||
private val postsRepo: PostsRepo,
|
||||
private val cachingChatId: IdChatIdentifier,
|
||||
private val targetChatId: IdChatIdentifier,
|
||||
private val targetChatIds: List<IdChatIdentifier>,
|
||||
private val deleteAfterPosting: Boolean = true
|
||||
) {
|
||||
suspend fun publish(postId: PostId) {
|
||||
suspend fun publish(postId: PostId): Boolean {
|
||||
val messagesInfo = postsRepo.getById(postId) ?: let {
|
||||
logger.w { "Unable to get post with id $postId for publishing" }
|
||||
return
|
||||
return false
|
||||
}
|
||||
val sortedMessagesContents = messagesInfo.content.groupBy { it.group }.flatMap { (group, list) ->
|
||||
if (group == null) {
|
||||
@@ -35,20 +34,26 @@ class PostPublisher(
|
||||
listOf(list.first().order to list)
|
||||
}
|
||||
}.sortedBy { it.first }
|
||||
var haveSentMessages = false
|
||||
|
||||
sortedMessagesContents.forEach { (_, contents) ->
|
||||
contents.singleOrNull() ?.also {
|
||||
runCatching {
|
||||
bot.copyMessage(targetChatId, it.chatId, it.messageId)
|
||||
}.onFailure { _ ->
|
||||
targetChatIds.forEach { targetChatId ->
|
||||
runCatching {
|
||||
bot.forwardMessage(
|
||||
it.chatId,
|
||||
targetChatId,
|
||||
it.messageId
|
||||
)
|
||||
bot.copyMessage(targetChatId, it.chatId, it.messageId)
|
||||
}.onFailure { _ ->
|
||||
runCatching {
|
||||
bot.forwardMessage(
|
||||
fromChatId = it.chatId,
|
||||
toChatId = cachingChatId,
|
||||
messageId = it.messageId
|
||||
)
|
||||
}.onSuccess {
|
||||
bot.copyMessage(targetChatId, it)
|
||||
haveSentMessages = true
|
||||
}
|
||||
}.onSuccess {
|
||||
bot.copyMessage(targetChatId, it)
|
||||
haveSentMessages = true
|
||||
}
|
||||
}
|
||||
return@forEach
|
||||
@@ -57,17 +62,26 @@ class PostPublisher(
|
||||
it.order to (bot.forwardMessage(toChatId = cachingChatId, fromChatId = it.chatId, messageId = it.messageId).contentMessageOrNull() ?: return@mapNotNull null)
|
||||
}.sortedBy { it.first }.mapNotNull { (_, forwardedMessage) ->
|
||||
forwardedMessage.withContentOrNull<MediaGroupPartContent>() ?: null.also { _ ->
|
||||
bot.copyMessage(targetChatId, forwardedMessage)
|
||||
targetChatIds.forEach { targetChatId ->
|
||||
bot.copyMessage(targetChatId, forwardedMessage)
|
||||
haveSentMessages = true
|
||||
}
|
||||
}
|
||||
}
|
||||
resultContents.singleOrNull() ?.also {
|
||||
bot.copyMessage(targetChatId, it)
|
||||
targetChatIds.forEach { targetChatId ->
|
||||
bot.copyMessage(targetChatId, it)
|
||||
haveSentMessages = true
|
||||
}
|
||||
return@forEach
|
||||
} ?: resultContents.chunked(mediaCountInMediaGroup.last).forEach {
|
||||
bot.send(
|
||||
targetChatId,
|
||||
it.map { it.content.toMediaGroupMemberTelegramMedia() }
|
||||
)
|
||||
targetChatIds.forEach { targetChatId ->
|
||||
bot.send(
|
||||
targetChatId,
|
||||
it.map { it.content.toMediaGroupMemberTelegramMedia() }
|
||||
)
|
||||
haveSentMessages = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,5 +89,6 @@ class PostPublisher(
|
||||
postsRepo.deleteById(postId)
|
||||
}
|
||||
|
||||
return haveSentMessages
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ 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 {
|
||||
@Serializable
|
||||
@@ -59,7 +58,7 @@ object Plugin : Plugin {
|
||||
}
|
||||
single {
|
||||
val config = get<Config>()
|
||||
PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.targetChatId, config.deleteAfterPublishing)
|
||||
PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.allTargetChatIds.toList(), config.deleteAfterPublishing)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package dev.inmo.plaguposter.posts.cached
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import dev.inmo.micro_utils.coroutines.SmartRWLocker
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.micro_utils.pagination.FirstPagePagination
|
||||
import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination
|
||||
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
|
||||
import dev.inmo.micro_utils.repos.CRUDRepo
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.MapKeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
|
||||
import dev.inmo.micro_utils.repos.cache.full.FullCRUDCacheRepo
|
||||
import dev.inmo.plaguposter.posts.models.NewPost
|
||||
@@ -13,23 +16,25 @@ import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.plaguposter.posts.models.RegisteredPost
|
||||
import dev.inmo.plaguposter.posts.repo.PostsRepo
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class CachedPostsRepo(
|
||||
private val parentRepo: PostsRepo,
|
||||
private val scope: CoroutineScope,
|
||||
private val kvCache: FullKVCache<PostId, RegisteredPost> = FullKVCache()
|
||||
private val kvCache: KeyValueRepo<PostId, RegisteredPost> = MapKeyValueRepo()
|
||||
) : PostsRepo, CRUDRepo<RegisteredPost, PostId, NewPost> by FullCRUDCacheRepo(
|
||||
parentRepo,
|
||||
kvCache,
|
||||
scope,
|
||||
skipStartInvalidate = false,
|
||||
locker = SmartRWLocker(),
|
||||
{ it.id }
|
||||
) {
|
||||
override val removedPostsFlow: Flow<RegisteredPost> by parentRepo::removedPostsFlow
|
||||
|
||||
override suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageIdentifier): PostId? {
|
||||
override suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageId): PostId? {
|
||||
doForAllWithNextPaging(firstPageWithOneElementPagination) {
|
||||
kvCache.values(it).also {
|
||||
it.results.forEach {
|
||||
|
||||
@@ -4,8 +4,7 @@ import com.benasher44.uuid.uuid4
|
||||
import dev.inmo.micro_utils.repos.KeyValuesRepo
|
||||
import dev.inmo.micro_utils.repos.exposed.*
|
||||
import dev.inmo.plaguposter.posts.models.*
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import org.jetbrains.exposed.sql.*
|
||||
|
||||
internal class ExposedContentInfoRepo(
|
||||
@@ -21,9 +20,9 @@ internal class ExposedContentInfoRepo(
|
||||
|
||||
val ResultRow.asObject
|
||||
get() = PostContentInfo(
|
||||
IdChatIdentifier(get(chatIdColumn), get(threadIdColumn)),
|
||||
get(messageIdColumn),
|
||||
get(groupColumn),
|
||||
IdChatIdentifier(RawChatId(get(chatIdColumn)), get(threadIdColumn) ?.let(::MessageThreadId)),
|
||||
MessageId(get(messageIdColumn)),
|
||||
get(groupColumn) ?.let(::MediaGroupId),
|
||||
get(orderColumn)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.inmo.plaguposter.posts.exposed
|
||||
|
||||
import com.benasher44.uuid.uuid4
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.micro_utils.repos.KeyValuesRepo
|
||||
import dev.inmo.micro_utils.repos.UpdatedValuePair
|
||||
import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo
|
||||
@@ -10,7 +10,7 @@ import dev.inmo.plaguposter.posts.models.*
|
||||
import dev.inmo.plaguposter.posts.repo.PostsRepo
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageId
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jetbrains.exposed.sql.*
|
||||
@@ -26,6 +26,7 @@ class ExposedPostsRepo(
|
||||
) {
|
||||
val idColumn = text("id")
|
||||
val createdColumn = double("datetime").default(0.0)
|
||||
val latestUpdateColumn = double("latest_update").default(0.0)
|
||||
|
||||
private val contentRepo by lazy {
|
||||
ExposedContentInfoRepo(
|
||||
@@ -81,7 +82,9 @@ class ExposedPostsRepo(
|
||||
return id
|
||||
}
|
||||
|
||||
override fun update(id: PostId?, value: NewPost, it: UpdateBuilder<Int>) {}
|
||||
override fun update(id: PostId?, value: NewPost, it: UpdateBuilder<Int>) {
|
||||
it[latestUpdateColumn] = DateTime.now().unixMillis
|
||||
}
|
||||
|
||||
private fun updateContent(post: RegisteredPost) {
|
||||
transaction(database) {
|
||||
@@ -90,10 +93,10 @@ class ExposedPostsRepo(
|
||||
post.content.forEach { contentInfo ->
|
||||
insert {
|
||||
it[postIdColumn] = post.id.string
|
||||
it[chatIdColumn] = contentInfo.chatId.chatId
|
||||
it[threadIdColumn] = contentInfo.chatId.threadId
|
||||
it[messageIdColumn] = contentInfo.messageId
|
||||
it[groupColumn] = contentInfo.group
|
||||
it[chatIdColumn] = contentInfo.chatId.chatId.long
|
||||
it[threadIdColumn] = contentInfo.chatId.threadId ?.long
|
||||
it[messageIdColumn] = contentInfo.messageId.long
|
||||
it[groupColumn] = contentInfo.group ?.string
|
||||
it[orderColumn] = contentInfo.order
|
||||
}
|
||||
}
|
||||
@@ -150,13 +153,13 @@ class ExposedPostsRepo(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageIdentifier): PostId? {
|
||||
override suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageId): PostId? {
|
||||
return transaction(database) {
|
||||
with(contentRepo) {
|
||||
select {
|
||||
chatIdColumn.eq(chatId.chatId)
|
||||
.and(chatId.threadId ?.let { threadIdColumn.eq(it) } ?: threadIdColumn.isNull())
|
||||
.and(messageIdColumn.eq(messageId))
|
||||
chatIdColumn.eq(chatId.chatId.long)
|
||||
.and(chatId.threadId ?.let { threadIdColumn.eq(it.long) } ?: threadIdColumn.isNull())
|
||||
.and(messageIdColumn.eq(messageId.long))
|
||||
}.limit(1).firstOrNull() ?.get(postIdColumn)
|
||||
} ?.let(::PostId)
|
||||
}
|
||||
|
||||
@@ -18,84 +18,95 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextWithFSM
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.*
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.strictlyOn
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
|
||||
import dev.inmo.tgbotapi.extensions.utils.extensions.raw.text
|
||||
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
|
||||
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
|
||||
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
|
||||
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
|
||||
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
|
||||
import dev.inmo.tgbotapi.types.message.content.MessageContent
|
||||
import dev.inmo.tgbotapi.utils.regular
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.core.Koin
|
||||
|
||||
@Serializable
|
||||
object Plugin : Plugin {
|
||||
@Serializable
|
||||
data class Config(
|
||||
val useInlineFinishingOpportunity: Boolean = true
|
||||
)
|
||||
|
||||
override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) {
|
||||
val config = koin.get<ChatConfig>()
|
||||
val postsRepo = koin.get<PostsRepo>()
|
||||
|
||||
strictlyOn {state: RegistrationState.InProcess ->
|
||||
strictlyOn { state: RegistrationState.InProcess ->
|
||||
val buttonUuid = "finish"
|
||||
|
||||
val messageToDelete = send(
|
||||
state.context,
|
||||
dev.inmo.tgbotapi.utils.buildEntities {
|
||||
if (state.messages.isNotEmpty()) {
|
||||
regular("Your message(s) has been registered. You may send new ones or push \"Finish\" to finalize your post")
|
||||
val suggestionMessageDeferred = async {
|
||||
send(
|
||||
state.context,
|
||||
dev.inmo.tgbotapi.utils.buildEntities {
|
||||
if (state.messages.isNotEmpty()) {
|
||||
regular("Your message(s) has been registered. You may send new ones or push \"Finish\" to finalize your post")
|
||||
} else {
|
||||
regular("Ok, send me your messages for new post")
|
||||
}
|
||||
},
|
||||
replyMarkup = if (state.messages.isNotEmpty()) {
|
||||
flatInlineKeyboard {
|
||||
dataButton(
|
||||
"Finish",
|
||||
buttonUuid
|
||||
)
|
||||
}
|
||||
} else {
|
||||
regular("Ok, send me your messages for new post")
|
||||
null
|
||||
}
|
||||
},
|
||||
replyMarkup = if (state.messages.isNotEmpty()) {
|
||||
flatInlineKeyboard {
|
||||
dataButton(
|
||||
"Finish",
|
||||
buttonUuid
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val newMessagesInfo = firstOf {
|
||||
firstOf {
|
||||
add {
|
||||
listOf(
|
||||
waitAnyContentMessage().filter {
|
||||
it.chat.id == state.context && it.content.textContentOrNull() ?.text != "/finish_post"
|
||||
}.take(1).first()
|
||||
)
|
||||
val receivedMessage = waitAnyContentMessage().filter {
|
||||
it.sameChat(state.context)
|
||||
}.first()
|
||||
|
||||
when {
|
||||
receivedMessage.content.textContentOrNull() ?.text == "/finish_post" -> {
|
||||
val messageToDelete = suggestionMessageDeferred.await()
|
||||
edit(messageToDelete, "Ok, finishing your request")
|
||||
RegistrationState.Finish(
|
||||
state.context,
|
||||
state.messages
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
RegistrationState.InProcess(
|
||||
state.context,
|
||||
state.messages + PostContentInfo.fromMessage(receivedMessage)
|
||||
).also {
|
||||
runCatchingSafely {
|
||||
suggestionMessageDeferred.cancel()
|
||||
}
|
||||
runCatchingSafely {
|
||||
delete(suggestionMessageDeferred.await())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
add {
|
||||
val messageToDelete = suggestionMessageDeferred.await()
|
||||
val finishPressed = waitMessageDataCallbackQuery().filter {
|
||||
it.message.sameMessage(messageToDelete) && it.data == buttonUuid
|
||||
}.first()
|
||||
emptyList<ContentMessage<MessageContent>>()
|
||||
}
|
||||
add {
|
||||
val finishPressed = waitTextMessage().filter {
|
||||
it.sameChat(messageToDelete) && it.content.text == "/finish_post"
|
||||
}.first()
|
||||
emptyList<ContentMessage<MessageContent>>()
|
||||
}
|
||||
}.ifEmpty {
|
||||
edit(messageToDelete, "Ok, finishing your request")
|
||||
return@strictlyOn RegistrationState.Finish(
|
||||
state.context,
|
||||
state.messages
|
||||
)
|
||||
}.flatMap {
|
||||
PostContentInfo.fromMessage(it)
|
||||
}
|
||||
|
||||
RegistrationState.InProcess(
|
||||
state.context,
|
||||
state.messages + newMessagesInfo
|
||||
).also {
|
||||
delete(messageToDelete)
|
||||
edit(messageToDelete, "Ok, finishing your request")
|
||||
RegistrationState.Finish(
|
||||
state.context,
|
||||
state.messages
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,12 +119,12 @@ object Plugin : Plugin {
|
||||
null
|
||||
}
|
||||
|
||||
onCommand("start_post", initialFilter = { it.chat.id == config.sourceChatId }) {
|
||||
onCommand("start_post", initialFilter = { config.allSourceChatIds.any { chatId -> it.sameChat(chatId) } }) {
|
||||
startChain(RegistrationState.InProcess(it.chat.id, emptyList()))
|
||||
}
|
||||
|
||||
onContentMessage(
|
||||
initialFilter = { it.chat.id == config.sourceChatId && !FirstSourceIsCommandsFilter(it) }
|
||||
initialFilter = { config.allSourceChatIds.any { chatId -> it.sameChat(chatId) } && !FirstSourceIsCommandsFilter(it) }
|
||||
) {
|
||||
startChain(RegistrationState.Finish(it.chat.id, PostContentInfo.fromMessage(it)))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
task javadocsJar(type: Jar) {
|
||||
classifier = 'javadoc'
|
||||
archiveClassifier = 'javadoc'
|
||||
}
|
||||
|
||||
publishing {
|
||||
@@ -79,4 +79,27 @@ if (project.hasProperty("signing.gnupg.keyName")) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
package dev.inmo.plaguposter.ratings.gc
|
||||
|
||||
import com.soywiz.klock.milliseconds
|
||||
import com.soywiz.klock.seconds
|
||||
import korlibs.time.DateTime
|
||||
import korlibs.time.seconds
|
||||
import dev.inmo.krontab.KrontabTemplate
|
||||
import dev.inmo.krontab.toSchedule
|
||||
import dev.inmo.krontab.utils.asFlow
|
||||
import dev.inmo.krontab.utils.asFlowWithDelays
|
||||
import dev.inmo.kslog.common.KSLog
|
||||
import dev.inmo.kslog.common.i
|
||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.koin.singleWithRandomQualifier
|
||||
import dev.inmo.micro_utils.repos.*
|
||||
import dev.inmo.plagubot.Plugin
|
||||
import dev.inmo.plagubot.plugins.inline.queries.models.Format
|
||||
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
|
||||
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.plaguposter.posts.repo.PostsRepo
|
||||
import dev.inmo.plaguposter.ratings.models.Rating
|
||||
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
|
||||
import dev.inmo.tgbotapi.types.MilliSeconds
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
|
||||
import dev.inmo.tgbotapi.types.Seconds
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.*
|
||||
@@ -43,21 +51,53 @@ object Plugin : Plugin {
|
||||
val config = koin.get<Config>()
|
||||
|
||||
config.immediateDrop ?.let { toDrop ->
|
||||
ratingsRepo.onNewValue.subscribeSafelyWithoutExceptions(this) {
|
||||
if (it.value <= toDrop) {
|
||||
postsRepo.deleteById(it.id)
|
||||
suspend fun checkAndOptionallyDrop(postId: PostId, rating: Rating) {
|
||||
if (rating <= toDrop) {
|
||||
postsRepo.deleteById(postId)
|
||||
}
|
||||
}
|
||||
ratingsRepo.getAll().forEach {
|
||||
runCatchingSafely {
|
||||
checkAndOptionallyDrop(it.key, it.value)
|
||||
}
|
||||
}
|
||||
ratingsRepo.onNewValue.subscribeSafelyWithoutExceptions(this) {
|
||||
checkAndOptionallyDrop(it.first, it.second)
|
||||
}
|
||||
}
|
||||
config.autoclear ?.let { autoclear ->
|
||||
autoclear.autoClearKrontab.toSchedule().asFlow().subscribeSafelyWithoutExceptions(scope) {
|
||||
val dropCreatedBefore = it - (autoclear.skipPostAge ?: 0).seconds
|
||||
ratingsRepo.getPostsWithRatingLessEq(autoclear.rating).keys.forEach {
|
||||
if ((postsRepo.getPostCreationTime(it) ?: return@forEach) < dropCreatedBefore) {
|
||||
postsRepo.deleteById(it)
|
||||
}
|
||||
val autoClearLogger = KSLog("autoclear")
|
||||
suspend fun doAutoClear() {
|
||||
autoClearLogger.i { "Start autoclear" }
|
||||
val dropCreatedBefore = DateTime.now() - (autoclear.skipPostAge ?: 0).seconds
|
||||
autoClearLogger.i { "Posts drop created before: ${dropCreatedBefore.toStringDefault()}" }
|
||||
val idsToDelete = ratingsRepo.getPostsWithRatingLessEq(autoclear.rating).keys.also {
|
||||
autoClearLogger.i { "Selected posts by rating: $it" }
|
||||
}.filter {
|
||||
val postCreationDateTime = postsRepo.getPostCreationTime(it) ?: (dropCreatedBefore - 1.seconds) // do dropping if post creation time is not available
|
||||
postCreationDateTime < dropCreatedBefore
|
||||
}
|
||||
autoClearLogger.i { "Filtered posts by datetime: $idsToDelete" }
|
||||
if (idsToDelete.isNotEmpty()) {
|
||||
runCatching { ratingsRepo.unset(idsToDelete) }
|
||||
autoClearLogger.i { "Ratings dropped" }
|
||||
runCatching { postsRepo.deleteById(idsToDelete) }
|
||||
autoClearLogger.i { "Posts dropped" }
|
||||
}
|
||||
}
|
||||
runCatchingSafely {
|
||||
doAutoClear()
|
||||
}
|
||||
autoclear.autoClearKrontab.toSchedule().asFlowWithDelays().subscribeSafelyWithoutExceptions(scope) {
|
||||
doAutoClear()
|
||||
}
|
||||
|
||||
onCommand("clean_posts_by_ratings") {
|
||||
doAutoClear()
|
||||
}
|
||||
koin.getOrNull<InlineTemplatesRepo>() ?.addTemplate(
|
||||
OfferTemplate("Force autoclear", listOf(Format("/clean_posts_by_ratings")))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package dev.inmo.plaguposter.ratings.selector
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.plaguposter.posts.repo.PostsRepo
|
||||
import dev.inmo.plaguposter.ratings.repo.RatingsRepo
|
||||
@@ -9,13 +10,14 @@ import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
|
||||
class DefaultSelector (
|
||||
private val config: SelectorConfig,
|
||||
private val ratingsRepo: RatingsRepo,
|
||||
private val postsRepo: PostsRepo
|
||||
private val postsRepo: PostsRepo,
|
||||
private val latestChosenRepo: KeyValueRepo<PostId, DateTime>
|
||||
) : Selector {
|
||||
override suspend fun take(n: Int, now: DateTime): List<PostId> {
|
||||
override suspend fun take(n: Int, now: DateTime, exclude: List<PostId>): List<PostId> {
|
||||
val result = mutableListOf<PostId>()
|
||||
|
||||
do {
|
||||
val selected = config.active(now.time) ?.rating ?.select(ratingsRepo, postsRepo, result, now) ?: break
|
||||
val selected = config.active(now.time) ?.rating ?.select(ratingsRepo, postsRepo, result + exclude, now, latestChosenRepo) ?: break
|
||||
result.add(selected)
|
||||
} while (result.size < n)
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package dev.inmo.plaguposter.ratings.selector
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
|
||||
interface Selector {
|
||||
suspend fun take(n: Int = 1, now: DateTime = DateTime.now()): List<PostId>
|
||||
suspend fun take(n: Int = 1, now: DateTime = DateTime.now(), exclude: List<PostId> = emptyList()): List<PostId>
|
||||
suspend fun takeOneOrNull(now: DateTime = DateTime.now(), exclude: List<PostId> = emptyList()): PostId? = take(
|
||||
n = 1,
|
||||
now = now,
|
||||
exclude = exclude
|
||||
).firstOrNull()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package dev.inmo.plaguposter.ratings.selector.models
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import com.soywiz.klock.seconds
|
||||
import dev.inmo.micro_utils.pagination.FirstPagePagination
|
||||
import dev.inmo.micro_utils.pagination.Pagination
|
||||
import korlibs.time.DateTime
|
||||
import korlibs.time.seconds
|
||||
import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging
|
||||
import dev.inmo.micro_utils.repos.pagination.getAll
|
||||
import dev.inmo.plaguposter.common.DateTimeSerializer
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
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.ratings.models.Rating
|
||||
@@ -25,17 +23,23 @@ data class RatingConfig(
|
||||
val max: Rating? = null,
|
||||
val prefer: Prefer = Prefer.Random,
|
||||
val otherwise: RatingConfig? = null,
|
||||
val postAge: Seconds? = null
|
||||
val postAge: Seconds? = null,
|
||||
val uniqueCount: Int? = null
|
||||
) {
|
||||
suspend fun select(
|
||||
ratingsRepo: RatingsRepo,
|
||||
postsRepo: PostsRepo,
|
||||
exclude: List<PostId>,
|
||||
now: DateTime
|
||||
now: DateTime,
|
||||
latestChosenRepo: KeyValueRepo<PostId, DateTime>
|
||||
): PostId? {
|
||||
var reversed: Boolean = false
|
||||
var count: Int? = null
|
||||
val allowedCreationTime = now - (postAge ?: 0).seconds
|
||||
val excludedByRepo = uniqueCount ?.let {
|
||||
latestChosenRepo.getAll().toList().sortedBy { it.second }.takeLast(uniqueCount).map { it.first }
|
||||
} ?: emptyList()
|
||||
val resultExcluded = exclude + excludedByRepo
|
||||
|
||||
when (prefer) {
|
||||
Prefer.Max -> {
|
||||
@@ -59,40 +63,53 @@ data class RatingConfig(
|
||||
ratingsRepo.getAllByWithNextPaging { keys(it) }
|
||||
}
|
||||
else -> {
|
||||
ratingsRepo.getPostsWithRatingLessEq(max, exclude = exclude).keys
|
||||
ratingsRepo.getPostsWithRatingLessEq(max, exclude = resultExcluded).keys
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
when (max) {
|
||||
null -> {
|
||||
ratingsRepo.getPostsWithRatingGreaterEq(min, exclude = exclude).keys
|
||||
ratingsRepo.getPostsWithRatingGreaterEq(min, exclude = resultExcluded).keys
|
||||
}
|
||||
else -> {
|
||||
ratingsRepo.getPosts(min .. max, reversed, count, exclude = exclude).keys
|
||||
ratingsRepo.getPosts(min .. max, reversed, count, exclude = resultExcluded).keys
|
||||
}
|
||||
}
|
||||
}
|
||||
}.filter {
|
||||
it !in exclude && (postsRepo.getPostCreationTime(it) ?.let { it < allowedCreationTime } ?: true)
|
||||
it !in resultExcluded && (postsRepo.getPostCreationTime(it) ?.let { it < allowedCreationTime } ?: true)
|
||||
}
|
||||
|
||||
return when (prefer) {
|
||||
val resultPosts: PostId = when (prefer) {
|
||||
Prefer.Max,
|
||||
Prefer.Min -> posts.firstOrNull()
|
||||
Prefer.Random -> posts.randomOrNull()
|
||||
} ?: otherwise ?.select(ratingsRepo, postsRepo, exclude, now)
|
||||
} ?: otherwise ?.select(ratingsRepo, postsRepo, resultExcluded, now, latestChosenRepo) ?: return null
|
||||
|
||||
val postsToKeep = uniqueCount ?.let {
|
||||
(excludedByRepo + resultPosts).takeLast(it)
|
||||
} ?: return resultPosts
|
||||
|
||||
val postsToRemoveFromKeep = excludedByRepo.filter { it !in postsToKeep }
|
||||
latestChosenRepo.unset(postsToRemoveFromKeep)
|
||||
val postsToAdd = postsToKeep.filter { it !in excludedByRepo }
|
||||
latestChosenRepo.set(
|
||||
postsToAdd.associateWith { DateTime.now() }
|
||||
)
|
||||
|
||||
return resultPosts
|
||||
}
|
||||
|
||||
@Serializable(Prefer.Serializer::class)
|
||||
sealed interface Prefer {
|
||||
val type: String
|
||||
@Serializable(Serializer::class)
|
||||
object Max : Prefer { override val type: String = "max" }
|
||||
data object Max : Prefer { override val type: String = "max" }
|
||||
@Serializable(Serializer::class)
|
||||
object Min : Prefer { override val type: String = "min" }
|
||||
data object Min : Prefer { override val type: String = "min" }
|
||||
@Serializable(Serializer::class)
|
||||
object Random : Prefer { override val type: String = "random" }
|
||||
data object Random : Prefer { override val type: String = "random" }
|
||||
|
||||
object Serializer : KSerializer<Prefer> {
|
||||
override val descriptor: SerialDescriptor = String.serializer().descriptor
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.inmo.plaguposter.ratings.selector.models
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import com.soywiz.klock.Time
|
||||
import korlibs.time.DateTime
|
||||
import korlibs.time.Time
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.plaguposter.ratings.selector.models
|
||||
|
||||
import com.soywiz.klock.*
|
||||
import korlibs.time.*
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
|
||||
@@ -1,14 +1,33 @@
|
||||
package dev.inmo.plaguposter.ratings.selector
|
||||
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.mappers.withMapper
|
||||
import dev.inmo.plagubot.Plugin
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
|
||||
import korlibs.time.DateTime
|
||||
import kotlinx.serialization.json.*
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.qualifier.qualifier
|
||||
|
||||
object Plugin : Plugin {
|
||||
override fun Module.setupDI(database: Database, params: JsonObject) {
|
||||
single { get<Json>().decodeFromJsonElement(SelectorConfig.serializer(), params["selector"] ?: return@single null) }
|
||||
single<Selector> { DefaultSelector(get(), get(), get()) }
|
||||
single<KeyValueRepo<PostId, DateTime>>(qualifier("latestChosenRepo")) {
|
||||
ExposedKeyValueRepo(
|
||||
get(),
|
||||
{ text("post_id") },
|
||||
{ double("date_time") },
|
||||
"LatestChosenRepo"
|
||||
).withMapper(
|
||||
{ string },
|
||||
{ unixMillis },
|
||||
{ PostId(this) },
|
||||
{ DateTime(this) }
|
||||
)
|
||||
}
|
||||
single<Selector> { DefaultSelector(get(), get(), get(), get(qualifier("latestChosenRepo"))) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.plaguposter.ratings.source.buttons
|
||||
|
||||
import com.soywiz.klock.DateFormat
|
||||
import korlibs.time.DateFormat
|
||||
import dev.inmo.kslog.common.TagLogger
|
||||
import dev.inmo.kslog.common.d
|
||||
import dev.inmo.kslog.common.i
|
||||
@@ -122,7 +122,7 @@ suspend fun BehaviourContext.includeRootNavigationButtonsHandler(
|
||||
|
||||
edit(
|
||||
it.message,
|
||||
onPageUpdate(SimplePagination(page, size), args.drop(3).toTypedArray()) ?: return@runCatchingSafely
|
||||
replyMarkup = onPageUpdate(SimplePagination(page, size), args.drop(3).toTypedArray()) ?: return@runCatchingSafely
|
||||
)
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ suspend fun BehaviourContext.includeRootNavigationButtonsHandler(
|
||||
if (prefix == RootButtonsShowRatingData) {
|
||||
runCatchingSafely {
|
||||
val rating = ratingRaw.toDoubleOrNull() ?: return@runCatchingSafely
|
||||
edit(it.message, ratingsRepo.buildRatingButtons(postsRepo, Rating(rating)))
|
||||
edit(it.message, replyMarkup = ratingsRepo.buildRatingButtons(postsRepo, Rating(rating)))
|
||||
}
|
||||
|
||||
answer(it)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package dev.inmo.plaguposter.ratings.source.repos
|
||||
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.cache.KeyValueCacheRepo
|
||||
import dev.inmo.micro_utils.repos.MapKeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
|
||||
import dev.inmo.micro_utils.repos.cache.full.cached
|
||||
import dev.inmo.micro_utils.repos.cache.full.fullyCached
|
||||
import dev.inmo.plaguposter.common.ShortMessageInfo
|
||||
import dev.inmo.tgbotapi.types.PollIdentifier
|
||||
import dev.inmo.tgbotapi.types.PollId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class CachedPollsToMessagesInfoRepo(
|
||||
private val repo: PollsToMessagesInfoRepo,
|
||||
private val scope: CoroutineScope,
|
||||
private val kvCache: FullKVCache<PollIdentifier, ShortMessageInfo> = FullKVCache()
|
||||
) : PollsToMessagesInfoRepo, KeyValueRepo<PollIdentifier, ShortMessageInfo> by repo.cached(kvCache, scope)
|
||||
private val kvCache: KeyValueRepo<PollId, ShortMessageInfo> = MapKeyValueRepo()
|
||||
) : PollsToMessagesInfoRepo, KeyValueRepo<PollId, ShortMessageInfo> by repo.fullyCached(kvCache, scope)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package dev.inmo.plaguposter.ratings.source.repos
|
||||
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.MapKeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
|
||||
import dev.inmo.micro_utils.repos.cache.full.cached
|
||||
import dev.inmo.plaguposter.common.ShortMessageInfo
|
||||
import dev.inmo.micro_utils.repos.cache.full.fullyCached
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.tgbotapi.types.PollIdentifier
|
||||
import dev.inmo.tgbotapi.types.PollId
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
||||
class CachedPollsToPostsIdsRepo(
|
||||
private val repo: PollsToPostsIdsRepo,
|
||||
private val scope: CoroutineScope,
|
||||
private val kvCache: FullKVCache<PollIdentifier, PostId> = FullKVCache()
|
||||
) : PollsToPostsIdsRepo, KeyValueRepo<PollIdentifier, PostId> by repo.cached(kvCache, scope)
|
||||
private val kvCache: KeyValueRepo<PollId, PostId> = MapKeyValueRepo()
|
||||
) : PollsToPostsIdsRepo, KeyValueRepo<PollId, PostId> by repo.fullyCached(kvCache, scope)
|
||||
|
||||
@@ -3,6 +3,6 @@ package dev.inmo.plaguposter.ratings.source.repos
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.plaguposter.common.ShortMessageInfo
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.tgbotapi.types.PollIdentifier
|
||||
import dev.inmo.tgbotapi.types.PollId
|
||||
|
||||
interface PollsToMessagesInfoRepo : KeyValueRepo<PollIdentifier, ShortMessageInfo>
|
||||
interface PollsToMessagesInfoRepo : KeyValueRepo<PollId, ShortMessageInfo>
|
||||
|
||||
@@ -2,6 +2,6 @@ package dev.inmo.plaguposter.ratings.source.repos
|
||||
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.tgbotapi.types.PollIdentifier
|
||||
import dev.inmo.tgbotapi.types.PollId
|
||||
|
||||
interface PollsToPostsIdsRepo : KeyValueRepo<PollIdentifier, PostId>
|
||||
interface PollsToPostsIdsRepo : KeyValueRepo<PollId, PostId>
|
||||
|
||||
@@ -28,6 +28,7 @@ import dev.inmo.plaguposter.ratings.utils.postsByRatings
|
||||
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.api.send.polls.sendRegularPoll
|
||||
import dev.inmo.tgbotapi.extensions.api.send.reply
|
||||
import dev.inmo.tgbotapi.extensions.api.send.send
|
||||
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
|
||||
@@ -37,10 +38,14 @@ import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
|
||||
import dev.inmo.tgbotapi.types.ReplyParameters
|
||||
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
|
||||
import dev.inmo.tgbotapi.types.message.textsources.bold
|
||||
import dev.inmo.tgbotapi.types.message.textsources.regular
|
||||
import dev.inmo.tgbotapi.types.polls.InputPollOption
|
||||
import dev.inmo.tgbotapi.types.polls.PollOption
|
||||
import dev.inmo.tgbotapi.utils.buildEntities
|
||||
import dev.inmo.tgbotapi.utils.extensions.makeSourceString
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -111,7 +116,7 @@ object Plugin : Plugin {
|
||||
onPollUpdates (markerFactory = { it.id }) { poll ->
|
||||
val postId = pollsToPostsIdsRepo.get(poll.id) ?: return@onPollUpdates
|
||||
val newRating = poll.options.sumOf {
|
||||
(variantsTransformer(it.text) ?.double ?.times(it.votes)) ?: 0.0
|
||||
(variantsTransformer(it.textSources.makeSourceString()) ?.double ?.times(it.votes)) ?: 0.0
|
||||
}
|
||||
ratingsRepo.set(postId, Rating(newRating))
|
||||
}
|
||||
@@ -128,8 +133,10 @@ object Plugin : Plugin {
|
||||
val sent = send(
|
||||
content.chatId,
|
||||
config.ratingOfferText,
|
||||
config.variants.keys.toList(),
|
||||
replyToMessageId = content.messageId
|
||||
options = config.variants.map {
|
||||
InputPollOption(it.key)
|
||||
},
|
||||
replyParameters = ReplyParameters(content.chatId, content.messageId)
|
||||
)
|
||||
pollsToPostsIdsRepo.set(sent.content.poll.id, postId)
|
||||
pollsToMessageInfoRepo.set(sent.content.poll.id, sent.short())
|
||||
@@ -242,7 +249,7 @@ object Plugin : Plugin {
|
||||
}
|
||||
}
|
||||
onCommand("ratings", requireOnlyCommandInMessage = true) {
|
||||
if (it.chat.id == chatConfig.sourceChatId) {
|
||||
if (it.chat.id in chatConfig.allSourceChatIds) {
|
||||
val ratings = ratingsRepo.postsByRatings().toList().sortedByDescending { it.first }
|
||||
val textSources = buildEntities {
|
||||
+ "Ratings amount: " + bold("${ratings.sumOf { it.second.size }}") + "\n\n"
|
||||
@@ -260,11 +267,11 @@ object Plugin : Plugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
includeRootNavigationButtonsHandler(setOf(chatConfig.sourceChatId), ratingsRepo, postsRepo)
|
||||
onMessageDataCallbackQuery("ratings_interactive", initialFilter = { it.message.chat.id == chatConfig.sourceChatId }) {
|
||||
includeRootNavigationButtonsHandler(chatConfig.allSourceChatIds, ratingsRepo, postsRepo)
|
||||
onMessageDataCallbackQuery("ratings_interactive", initialFilter = { it.message.chat.id in chatConfig.allSourceChatIds }) {
|
||||
edit(
|
||||
it.message,
|
||||
ratingsRepo.buildRootButtons()
|
||||
replyMarkup = ratingsRepo.buildRootButtons()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,7 @@ package dev.inmo.plaguposter.ratings.source.repos
|
||||
import dev.inmo.micro_utils.repos.exposed.initTable
|
||||
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
|
||||
import dev.inmo.plaguposter.common.ShortMessageInfo
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.IdChatIdentifier
|
||||
import dev.inmo.tgbotapi.types.PollIdentifier
|
||||
import dev.inmo.tgbotapi.types.*
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull
|
||||
@@ -13,7 +11,7 @@ import org.jetbrains.exposed.sql.statements.*
|
||||
|
||||
class ExposedPollsToMessagesInfoRepo(
|
||||
database: Database
|
||||
) : PollsToMessagesInfoRepo, AbstractExposedKeyValueRepo<PollIdentifier, ShortMessageInfo>(
|
||||
) : PollsToMessagesInfoRepo, AbstractExposedKeyValueRepo<PollId, ShortMessageInfo>(
|
||||
database,
|
||||
"polls_to_their_messages_info"
|
||||
) {
|
||||
@@ -21,31 +19,32 @@ class ExposedPollsToMessagesInfoRepo(
|
||||
private val chatIdColumn = long("chat_id")
|
||||
private val threadIdColumn = long("thread_id").nullable().default(null)
|
||||
private val messageIdColumn = long("message_id")
|
||||
override val selectById: ISqlExpressionBuilder.(PollIdentifier) -> Op<Boolean> = { keyColumn.eq(it) }
|
||||
override val selectById: ISqlExpressionBuilder.(PollId) -> Op<Boolean> = { keyColumn.eq(it.string) }
|
||||
override val selectByValue: ISqlExpressionBuilder.(ShortMessageInfo) -> Op<Boolean> = {
|
||||
chatIdColumn.eq(it.chatId.chatId).and(it.chatId.threadId ?.let { threadIdColumn.eq(it) } ?: threadIdColumn.isNull()).and(
|
||||
messageIdColumn.eq(it.messageId)
|
||||
chatIdColumn.eq(it.chatId.chatId.long)
|
||||
.and(it.chatId.threadId?.let { threadIdColumn.eq(it.long) } ?: threadIdColumn.isNull()).and(
|
||||
messageIdColumn.eq(it.messageId.long)
|
||||
)
|
||||
}
|
||||
override val ResultRow.asKey: PollIdentifier
|
||||
get() = get(keyColumn)
|
||||
override val ResultRow.asKey: PollId
|
||||
get() = PollId(get(keyColumn))
|
||||
override val ResultRow.asObject: ShortMessageInfo
|
||||
get() = ShortMessageInfo(
|
||||
IdChatIdentifier(get(chatIdColumn), get(threadIdColumn)),
|
||||
get(messageIdColumn)
|
||||
IdChatIdentifier(RawChatId(get(chatIdColumn)), get(threadIdColumn) ?.let(::MessageThreadId)),
|
||||
MessageId(get(messageIdColumn))
|
||||
)
|
||||
|
||||
init {
|
||||
initTable()
|
||||
}
|
||||
|
||||
override fun update(k: PollIdentifier, v: ShortMessageInfo, it: UpdateBuilder<Int>) {
|
||||
it[chatIdColumn] = v.chatId.chatId
|
||||
it[threadIdColumn] = v.chatId.threadId
|
||||
it[messageIdColumn] = v.messageId
|
||||
override fun update(k: PollId, v: ShortMessageInfo, it: UpdateBuilder<Int>) {
|
||||
it[chatIdColumn] = v.chatId.chatId.long
|
||||
it[threadIdColumn] = v.chatId.threadId ?.long
|
||||
it[messageIdColumn] = v.messageId.long
|
||||
}
|
||||
|
||||
override fun insertKey(k: PollIdentifier, v: ShortMessageInfo, it: InsertStatement<Number>) {
|
||||
it[keyColumn] = k
|
||||
override fun insertKey(k: PollId, v: ShortMessageInfo, it: InsertStatement<Number>) {
|
||||
it[keyColumn] = k.string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@ package dev.inmo.plaguposter.ratings.source.repos
|
||||
import dev.inmo.micro_utils.repos.exposed.initTable
|
||||
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.tgbotapi.types.PollIdentifier
|
||||
import dev.inmo.tgbotapi.types.PollId
|
||||
import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.statements.*
|
||||
|
||||
class ExposedPollsToPostsIdsRepo(
|
||||
database: Database
|
||||
) : PollsToPostsIdsRepo, AbstractExposedKeyValueRepo<PollIdentifier, PostId>(database, "polls_to_posts") {
|
||||
) : PollsToPostsIdsRepo, AbstractExposedKeyValueRepo<PollId, PostId>(database, "polls_to_posts") {
|
||||
override val keyColumn = text("poll_id")
|
||||
val postIdColumn = text("postId")
|
||||
override val selectById: ISqlExpressionBuilder.(PollIdentifier) -> Op<Boolean> = { keyColumn.eq(it) }
|
||||
override val selectById: ISqlExpressionBuilder.(PollId) -> Op<Boolean> = { keyColumn.eq(it.string) }
|
||||
override val selectByValue: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { postIdColumn.eq(it.string) }
|
||||
override val ResultRow.asKey: PollIdentifier
|
||||
get() = get(keyColumn)
|
||||
override val ResultRow.asKey: PollId
|
||||
get() = PollId(get(keyColumn))
|
||||
override val ResultRow.asObject: PostId
|
||||
get() = get(postIdColumn).let(::PostId)
|
||||
|
||||
@@ -23,11 +23,11 @@ class ExposedPollsToPostsIdsRepo(
|
||||
initTable()
|
||||
}
|
||||
|
||||
override fun update(k: PollIdentifier, v: PostId, it: UpdateBuilder<Int>) {
|
||||
override fun update(k: PollId, v: PostId, it: UpdateBuilder<Int>) {
|
||||
it[postIdColumn] = v.string
|
||||
}
|
||||
|
||||
override fun insertKey(k: PollIdentifier, v: PostId, it: InsertStatement<Number>) {
|
||||
it[keyColumn] = k
|
||||
override fun insertKey(k: PollId, v: PostId, it: InsertStatement<Number>) {
|
||||
it[keyColumn] = k.string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package dev.inmo.plaguposter.ratings.repo
|
||||
|
||||
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
|
||||
import dev.inmo.micro_utils.pagination.utils.optionallyReverse
|
||||
import dev.inmo.micro_utils.pagination.utils.paginate
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
|
||||
import dev.inmo.micro_utils.repos.MapKeyValueRepo
|
||||
import dev.inmo.micro_utils.repos.cache.full.FullKeyValueCacheRepo
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.plaguposter.ratings.models.Rating
|
||||
@@ -11,28 +13,32 @@ import kotlinx.coroutines.CoroutineScope
|
||||
class CachedRatingsRepo(
|
||||
private val base: RatingsRepo,
|
||||
private val scope: CoroutineScope,
|
||||
private val kvCache: FullKVCache<PostId, Rating> = FullKVCache()
|
||||
private val kvCache: MapKeyValueRepo<PostId, Rating> = MapKeyValueRepo()
|
||||
) : RatingsRepo, KeyValueRepo<PostId, Rating> by FullKeyValueCacheRepo(base, kvCache, scope) {
|
||||
private suspend fun getPosts(
|
||||
reversed: Boolean,
|
||||
count: Int?,
|
||||
exclude: List<PostId>,
|
||||
ratingFilter: (Rating) -> Boolean
|
||||
): Map<PostId, Rating> {
|
||||
return kvCache.getAll().filter { (it, rating) ->
|
||||
it !in exclude && ratingFilter(rating)
|
||||
}.let {
|
||||
if (count == null) {
|
||||
it
|
||||
} else {
|
||||
val keys = it.keys.optionallyReverse(reversed).take(count)
|
||||
keys.associateWith { id -> it.getValue(id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
override suspend fun getPosts(
|
||||
range: ClosedRange<Rating>,
|
||||
reversed: Boolean,
|
||||
count: Int?,
|
||||
exclude: List<PostId>
|
||||
): Map<PostId, Rating> {
|
||||
val result = mutableMapOf<PostId, Rating>()
|
||||
|
||||
doForAllWithNextPaging {
|
||||
kvCache.keys(it).also {
|
||||
it.results.forEach {
|
||||
val rating = get(it) ?: return@forEach
|
||||
if (it !in exclude && rating in range) {
|
||||
result[it] = rating
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.toMap()
|
||||
): Map<PostId, Rating> = getPosts(reversed, count, exclude) {
|
||||
it in range
|
||||
}
|
||||
|
||||
override suspend fun getPostsWithRatingGreaterEq(
|
||||
@@ -40,22 +46,16 @@ class CachedRatingsRepo(
|
||||
reversed: Boolean,
|
||||
count: Int?,
|
||||
exclude: List<PostId>
|
||||
): Map<PostId, Rating> = getPosts(
|
||||
then .. Rating(Double.MAX_VALUE),
|
||||
reversed,
|
||||
count,
|
||||
exclude
|
||||
)
|
||||
): Map<PostId, Rating> = getPosts(reversed, count, exclude) {
|
||||
it >= then
|
||||
}
|
||||
|
||||
override suspend fun getPostsWithRatingLessEq(
|
||||
then: Rating,
|
||||
reversed: Boolean,
|
||||
count: Int?,
|
||||
exclude: List<PostId>
|
||||
): Map<PostId, Rating> = getPosts(
|
||||
Rating(Double.MIN_VALUE) .. then,
|
||||
reversed,
|
||||
count,
|
||||
exclude
|
||||
)
|
||||
): Map<PostId, Rating> = getPosts(reversed, count, exclude) {
|
||||
it <= then
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import dev.inmo.micro_utils.koin.singleWithBinds
|
||||
import dev.inmo.micro_utils.repos.unset
|
||||
import dev.inmo.plagubot.Plugin
|
||||
import dev.inmo.plaguposter.common.useCache
|
||||
import dev.inmo.plaguposter.posts.exposed.ExposedPostsRepo
|
||||
import dev.inmo.plaguposter.posts.repo.PostsRepo
|
||||
import dev.inmo.plaguposter.ratings.exposed.ExposedRatingsRepo
|
||||
import dev.inmo.plaguposter.ratings.repo.*
|
||||
@@ -14,7 +13,6 @@ 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) {
|
||||
|
||||
@@ -49,7 +49,7 @@ class ExposedRatingsRepo (
|
||||
count: Int?,
|
||||
exclude: List<PostId>
|
||||
): Map<PostId, Rating> = transaction(database) {
|
||||
select {
|
||||
selectAll().where {
|
||||
ratingsColumn.greaterEq(range.start.double).and(
|
||||
ratingsColumn.lessEq(range.endInclusive.double)
|
||||
).and(
|
||||
@@ -66,7 +66,7 @@ class ExposedRatingsRepo (
|
||||
count: Int?,
|
||||
exclude: List<PostId>
|
||||
) = transaction(database) {
|
||||
select { ratingsColumn.greaterEq(then.double).and(keyColumn.notInList(exclude.map { it.string })) }.optionallyLimit(count).optionallyReverse(reversed).map {
|
||||
selectAll().where { ratingsColumn.greaterEq(then.double).and(keyColumn.notInList(exclude.map { it.string })) }.optionallyLimit(count).optionallyReverse(reversed).map {
|
||||
it.asKey to it.asObject
|
||||
}
|
||||
}.toMap()
|
||||
@@ -77,7 +77,7 @@ class ExposedRatingsRepo (
|
||||
count: Int?,
|
||||
exclude: List<PostId>
|
||||
): Map<PostId, Rating> = transaction(database) {
|
||||
select { ratingsColumn.lessEq(then.double).and(keyColumn.notInList(exclude.map { it.string })) }.optionallyLimit(count).optionallyReverse(reversed).map {
|
||||
selectAll().where { ratingsColumn.lessEq(then.double).and(keyColumn.notInList(exclude.map { it.string })) }.optionallyLimit(count).optionallyReverse(reversed).map {
|
||||
it.asKey to it.asObject
|
||||
}
|
||||
}.toMap()
|
||||
|
||||
@@ -22,6 +22,7 @@ dependencies {
|
||||
api project(":plaguposter.ratings.source")
|
||||
api project(":plaguposter.ratings.selector")
|
||||
api project(":plaguposter.ratings.gc")
|
||||
api project(":plaguposter.posts.gc")
|
||||
api project(":plaguposter.inlines")
|
||||
|
||||
api libs.psql
|
||||
@@ -32,6 +33,6 @@ application {
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"database": {
|
||||
"url": "jdbc:postgresql://127.0.0.1:8091/test",
|
||||
"username": "test",
|
||||
"password": "test",
|
||||
"driver": "org.postgresql.Driver"
|
||||
},
|
||||
"botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
|
||||
"plugins": [
|
||||
"dev.inmo.plaguposter.posts.Plugin",
|
||||
"dev.inmo.plaguposter.posts.registrar.Plugin",
|
||||
"dev.inmo.plaguposter.ratings.Plugin",
|
||||
"dev.inmo.plaguposter.ratings.source.Plugin",
|
||||
"dev.inmo.plaguposter.ratings.selector.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.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": {
|
||||
"chats": {
|
||||
"targetChat": 12345678,
|
||||
"cacheChat": 12345678,
|
||||
"sourceChat": 12345678
|
||||
}
|
||||
},
|
||||
"ratingsPolls": {
|
||||
"variants": {
|
||||
"Круть": 2,
|
||||
"Ок": 1,
|
||||
"Не ок": -1,
|
||||
"Совсем не ок": -2,
|
||||
"Посмотреть результаты": 0
|
||||
},
|
||||
"autoAttach": true,
|
||||
"ratingOfferText": "What do you think about it?"
|
||||
},
|
||||
"selector": {
|
||||
"items": [
|
||||
{
|
||||
"time": {
|
||||
"from": "00:00",
|
||||
"to": "23:59"
|
||||
},
|
||||
"rating": {
|
||||
"prefer": "max"
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": {
|
||||
"from": "23:59",
|
||||
"to": "00:00"
|
||||
},
|
||||
"rating": {
|
||||
"prefer": "max"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"timer_trigger": {
|
||||
"krontab": "0 30 2/4 * *"
|
||||
},
|
||||
"panel": {
|
||||
"textPrefix": "Post management:",
|
||||
"buttonsPerRow": 2,
|
||||
"parseMode": "MarkdownV2",
|
||||
"deleteButtonText": "Delete"
|
||||
},
|
||||
"publish_command": {
|
||||
"panelButtonText": "Publish"
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ function assert_success() {
|
||||
|
||||
app=plaguposter
|
||||
version="`grep ../gradle.properties -e "^version=" | sed -e "s/version=\(.*\)/\1/"`"
|
||||
server=docker.io/insanusmokrassar
|
||||
server=docker.inmo.dev
|
||||
|
||||
assert_success ../gradlew build
|
||||
assert_success sudo docker build -t $app:"$version" .
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
DATA_PATH=.
|
||||
|
||||
PG_USER=test_user
|
||||
PG_PASSWORD=test_password
|
||||
PG_DB=test_db
|
||||
@@ -1,131 +1,85 @@
|
||||
{
|
||||
"database": {
|
||||
"url": "jdbc:postgresql://postgres/test_db",
|
||||
"username": "test_user",
|
||||
"password": "test_password",
|
||||
"url": "jdbc:postgresql://postgres:5432/test",
|
||||
"username": "test",
|
||||
"password": "test",
|
||||
"driver": "org.postgresql.Driver"
|
||||
},
|
||||
"botToken": "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi",
|
||||
"botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
|
||||
"plugins": [
|
||||
"dev.inmo.plaguposter.posts.Plugin",
|
||||
"dev.inmo.plaguposter.posts.registrar.Plugin",
|
||||
"dev.inmo.plaguposter.ratings.Plugin",
|
||||
"dev.inmo.plaguposter.ratings.source.Plugin",
|
||||
"dev.inmo.plaguposter.ratings.selector.Plugin",
|
||||
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
|
||||
"dev.inmo.plaguposter.ratings.gc.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.common.CommonPlugin",
|
||||
"dev.inmo.plaguposter.triggers.timer.Plugin",
|
||||
"dev.inmo.plaguposter.triggers.timer.disablers.ratings.Plugin",
|
||||
"dev.inmo.plaguposter.triggers.timer.disablers.autoposts.Plugin",
|
||||
"dev.inmo.plaguposter.posts.gc.Plugin"
|
||||
],
|
||||
"posts": {
|
||||
"chats": {
|
||||
"targetChat": -1001234567890,
|
||||
"cacheChat": -1001234567890,
|
||||
"sourceChat": -1001234567890
|
||||
},
|
||||
"autoRemoveMessages": true
|
||||
"targetChat": 12345678,
|
||||
"cacheChat": 12345678,
|
||||
"sourceChat": 12345678,
|
||||
"targetChats": [12345678],
|
||||
"_note": "You must set targetChat or targetChats with at least one object"
|
||||
}
|
||||
},
|
||||
"ratingsPolls": {
|
||||
"variants": {
|
||||
"Круть": 2,
|
||||
"Ок": 1,
|
||||
"Не ок": -1,
|
||||
"Совсем не ок": -2,
|
||||
"Посмотреть результаты": 0
|
||||
"Cool": 2,
|
||||
"Ok": 1,
|
||||
"Not ok": -1,
|
||||
"Inappropriate": -2,
|
||||
"Results": 0
|
||||
},
|
||||
"autoAttach": true,
|
||||
"ratingOfferText": "How do you like it?"
|
||||
"ratingOfferText": "What do you think about it?"
|
||||
},
|
||||
"selector": {
|
||||
"items": [
|
||||
{
|
||||
"time": {
|
||||
"from": "23:00",
|
||||
"from": "00:00",
|
||||
"to": "23:59"
|
||||
},
|
||||
"rating": {
|
||||
"min": -1.0,
|
||||
"max": 2.0,
|
||||
"prefer": "max",
|
||||
"otherwise": {
|
||||
"rating": {
|
||||
"min": 2.0,
|
||||
"prefer": "min",
|
||||
"postAge": 86400
|
||||
}
|
||||
},
|
||||
"postAge": 86400
|
||||
"uniqueCount": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": {
|
||||
"from": "00:00",
|
||||
"to": "06:59"
|
||||
},
|
||||
"rating": {
|
||||
"min": -1.0,
|
||||
"max": 2.0,
|
||||
"prefer": "max",
|
||||
"otherwise": {
|
||||
"rating": {
|
||||
"min": 2.0,
|
||||
"prefer": "min",
|
||||
"postAge": 86400
|
||||
}
|
||||
},
|
||||
"postAge": 86400
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": {
|
||||
"from": "07:00",
|
||||
"to": "12:00"
|
||||
},
|
||||
"rating": {
|
||||
"min": 1.0,
|
||||
"prefer": "min",
|
||||
"otherwise": {
|
||||
"rating": {
|
||||
"max": 1.0,
|
||||
"prefer": "max",
|
||||
"postAge": 86400
|
||||
}
|
||||
},
|
||||
"postAge": 86400
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": {
|
||||
"from": "12:00",
|
||||
"to": "16:00"
|
||||
},
|
||||
"rating": {
|
||||
"min": 2.0,
|
||||
"prefer": "min",
|
||||
"otherwise": {
|
||||
"rating": {
|
||||
"max": 2.0,
|
||||
"prefer": "max",
|
||||
"postAge": 86400
|
||||
}
|
||||
},
|
||||
"postAge": 86400
|
||||
}
|
||||
},
|
||||
{
|
||||
"time": {
|
||||
"from": "16:00",
|
||||
"to": "23:00"
|
||||
"from": "23:59",
|
||||
"to": "00:00"
|
||||
},
|
||||
"rating": {
|
||||
"prefer": "max",
|
||||
"postAge": 86400
|
||||
"uniqueCount": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"timer_trigger": {
|
||||
"krontab": "0 30 */5 * *"
|
||||
"krontab": "0 30 2/4 * *",
|
||||
"retryOnPostFailureTimes": 0,
|
||||
"_note": "retryOnPostFailureTimes will retry to publish one or several posts if posting has been failed"
|
||||
},
|
||||
"panel": {
|
||||
"textPrefix": "Post management:",
|
||||
"buttonsPerRow": 2,
|
||||
"parseMode": "MarkdownV2",
|
||||
"deleteButtonText": "Delete"
|
||||
},
|
||||
"publish_command": {
|
||||
"panelButtonText": "Publish"
|
||||
},
|
||||
"gc": {
|
||||
"autoclear": {
|
||||
@@ -133,6 +87,11 @@
|
||||
"autoClearKrontab": "0 0 0 * *",
|
||||
"skipPostAge": 86400
|
||||
},
|
||||
"immediateDrop": -2
|
||||
"immediateDrop": -6
|
||||
},
|
||||
"messagesChecker": {
|
||||
"krontab": "0 0 0 * *",
|
||||
"throttlingMillis": 1000,
|
||||
"doFullCheck": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,24 @@ version: "3.4"
|
||||
|
||||
services:
|
||||
plaguposter_postgres:
|
||||
image: postgres
|
||||
image: postgres:15.4-bullseye
|
||||
container_name: "plaguposter_postgres"
|
||||
restart: "unless-stopped"
|
||||
environment:
|
||||
POSTGRES_USER: "${PG_USER}"
|
||||
POSTGRES_PASSWORD: "${PG_PASSWORD}"
|
||||
POSTGRES_DB: "${PG_DB}"
|
||||
POSTGRES_USER: "test"
|
||||
POSTGRES_PASSWORD: "test"
|
||||
POSTGRES_DB: "test"
|
||||
volumes:
|
||||
- "${DATA_PATH}/db/:/var/lib/postgresql/"
|
||||
- "./db/:/var/lib/postgresql/data"
|
||||
- "/etc/timezone:/etc/timezone:ro"
|
||||
plaguposter:
|
||||
image: insanusmokrassar/plaguposter
|
||||
image: insanusmokrassar/plaguposter:latest
|
||||
container_name: "plaguposter"
|
||||
restart: "unless-stopped"
|
||||
volumes:
|
||||
- "${DATA_PATH}/config.json:/config.json"
|
||||
links:
|
||||
- "plaguposter_postgres:postgres"
|
||||
- "./config.json:/config.json"
|
||||
- "/etc/timezone:/etc/timezone:ro"
|
||||
depends_on:
|
||||
- "plaguposter_postgres"
|
||||
links:
|
||||
- "plaguposter_postgres:postgres"
|
||||
|
||||
@@ -4,6 +4,7 @@ String[] includes = [
|
||||
":common",
|
||||
":posts",
|
||||
":posts:panel",
|
||||
":posts:gc",
|
||||
":posts_registrar",
|
||||
":ratings",
|
||||
":ratings:source",
|
||||
@@ -11,7 +12,6 @@ String[] includes = [
|
||||
":ratings:gc",
|
||||
":triggers:command",
|
||||
":triggers:selector_with_timer",
|
||||
":triggers:selector_with_scheduling",
|
||||
":triggers:timer",
|
||||
":triggers:timer:disablers:ratings",
|
||||
":triggers:timer:disablers:autoposts",
|
||||
@@ -30,5 +30,3 @@ includes.each { originalName ->
|
||||
project.name = projectName
|
||||
project.projectDir = new File(projectDirectory)
|
||||
}
|
||||
|
||||
enableFeaturePreview("VERSION_CATALOGS")
|
||||
|
||||
@@ -24,7 +24,7 @@ import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
|
||||
import dev.inmo.tgbotapi.types.ChatId
|
||||
import dev.inmo.tgbotapi.types.MessageIdentifier
|
||||
import dev.inmo.tgbotapi.types.MessageId
|
||||
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
|
||||
import dev.inmo.tgbotapi.types.message.textsources.regular
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -38,8 +38,8 @@ object Plugin : Plugin {
|
||||
@Serializable
|
||||
private data class PublishState(
|
||||
override val context: ChatId,
|
||||
val sourceMessageId: MessageIdentifier,
|
||||
val messageInReply: MessageIdentifier
|
||||
val sourceMessageId: MessageId,
|
||||
val messageInReply: MessageId
|
||||
) : State
|
||||
@Serializable
|
||||
internal data class Config(
|
||||
@@ -77,7 +77,7 @@ object Plugin : Plugin {
|
||||
|
||||
return@onCommand
|
||||
}
|
||||
} ?: selector ?.take(1) ?.firstOrNull()
|
||||
} ?: selector ?.takeOneOrNull()
|
||||
if (postId == null) {
|
||||
reply(
|
||||
it,
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package dev.inmo.plaguposter.triggers.selector_with_scheduling
|
||||
@@ -1,11 +0,0 @@
|
||||
package dev.inmo.plaguposter.triggers.selector_with_scheduling
|
||||
|
||||
import dev.inmo.plagubot.Plugin
|
||||
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) {
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<manifest package="dev.inmo.plaguposter.triggers.selector_with_scheduling"/>
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.plaguposter.triggers.selector_with_timer
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
|
||||
fun interface AutopostFilter {
|
||||
|
||||
@@ -1,14 +1,43 @@
|
||||
package dev.inmo.plaguposter.triggers.selector_with_timer
|
||||
|
||||
import korlibs.time.DateFormat
|
||||
import dev.inmo.krontab.KrontabTemplate
|
||||
import dev.inmo.krontab.toSchedule
|
||||
import dev.inmo.krontab.utils.asFlow
|
||||
import dev.inmo.krontab.utils.asFlowWithDelays
|
||||
import dev.inmo.krontab.utils.asFlowWithoutDelays
|
||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.koin.singleWithRandomQualifier
|
||||
import dev.inmo.micro_utils.pagination.FirstPagePagination
|
||||
import dev.inmo.micro_utils.pagination.Pagination
|
||||
import dev.inmo.micro_utils.pagination.firstIndex
|
||||
import dev.inmo.micro_utils.pagination.lastIndexExclusive
|
||||
import dev.inmo.plagubot.Plugin
|
||||
import dev.inmo.plagubot.plugins.inline.queries.models.Format
|
||||
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
|
||||
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
|
||||
import dev.inmo.plaguposter.common.ChatConfig
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
import dev.inmo.plaguposter.posts.repo.ReadPostsRepo
|
||||
import dev.inmo.plaguposter.posts.sending.PostPublisher
|
||||
import dev.inmo.plaguposter.ratings.selector.Selector
|
||||
import dev.inmo.tgbotapi.extensions.api.answers.answer
|
||||
import dev.inmo.tgbotapi.extensions.api.edit.edit
|
||||
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 dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
|
||||
import dev.inmo.tgbotapi.extensions.utils.formatting.makeLinkToMessage
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
|
||||
import dev.inmo.tgbotapi.extensions.utils.types.buttons.urlButton
|
||||
import dev.inmo.tgbotapi.types.BotCommand
|
||||
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
|
||||
import dev.inmo.tgbotapi.utils.row
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.collectIndexed
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.*
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
@@ -16,15 +45,22 @@ import org.koin.core.Koin
|
||||
import org.koin.core.module.Module
|
||||
|
||||
object Plugin : Plugin {
|
||||
@Serializable
|
||||
private const val pageCallbackDataQueryPrefix = "publishing_autoschedule page"
|
||||
private const val pageCallbackDataQuerySize = 5
|
||||
@Serializable
|
||||
internal data class Config(
|
||||
@SerialName("krontab")
|
||||
val krontabTemplate: KrontabTemplate
|
||||
val krontabTemplate: KrontabTemplate,
|
||||
val dateTimeFormat: String = "HH:mm:ss, dd.MM.yyyy",
|
||||
val retryOnPostFailureTimes: Int = 0
|
||||
) {
|
||||
@Transient
|
||||
val krontab by lazy {
|
||||
krontabTemplate.toSchedule()
|
||||
}
|
||||
|
||||
@Transient
|
||||
val format: DateFormat = DateFormat(dateTimeFormat)
|
||||
}
|
||||
override fun Module.setupDI(database: Database, params: JsonObject) {
|
||||
single { get<Json>().decodeFromJsonElement(Config.serializer(), params["timer_trigger"] ?: return@single null) }
|
||||
@@ -35,11 +71,116 @@ object Plugin : Plugin {
|
||||
val publisher = koin.get<PostPublisher>()
|
||||
val selector = koin.get<Selector>()
|
||||
val filters = koin.getAll<AutopostFilter>().distinct()
|
||||
koin.get<Config>().krontab.asFlow().subscribeSafelyWithoutExceptions(this) { dateTime ->
|
||||
selector.take(now = dateTime).forEach { postId ->
|
||||
if (filters.all { it.check(postId, dateTime) }) {
|
||||
publisher.publish(postId)
|
||||
val chatConfig = koin.get<ChatConfig>()
|
||||
val postsRepo = koin.get<ReadPostsRepo>()
|
||||
|
||||
koin.getOrNull<InlineTemplatesRepo>() ?.apply {
|
||||
addTemplate(
|
||||
OfferTemplate(
|
||||
"Autoschedule buttons",
|
||||
listOf(
|
||||
Format(
|
||||
"/autoschedule_panel"
|
||||
)
|
||||
),
|
||||
"Show autoscheduling publishing info"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val krontab = koin.get<Config>().krontab
|
||||
val retryOnPostFailureTimes = koin.get<Config>().retryOnPostFailureTimes
|
||||
val dateTimeFormat = koin.get<Config>().format
|
||||
krontab.asFlowWithDelays().subscribeSafelyWithoutExceptions(this) { dateTime ->
|
||||
var leftRetries = retryOnPostFailureTimes
|
||||
do {
|
||||
val success = runCatching {
|
||||
selector.takeOneOrNull(now = dateTime) ?.let { postId ->
|
||||
if (filters.all { it.check(postId, dateTime) }) {
|
||||
publisher.publish(postId)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} ?: false
|
||||
}.getOrElse {
|
||||
false
|
||||
}
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
leftRetries--;
|
||||
} while (leftRetries > 0)
|
||||
}
|
||||
|
||||
suspend fun buildPage(pagination: Pagination = FirstPagePagination(size = pageCallbackDataQuerySize)): InlineKeyboardMarkup {
|
||||
return inlineKeyboard {
|
||||
row {
|
||||
if (pagination.page > 1) {
|
||||
dataButton("⬅️", "${pageCallbackDataQueryPrefix}0")
|
||||
}
|
||||
if (pagination.page > 0) {
|
||||
dataButton("◀️", "${pageCallbackDataQueryPrefix}${pagination.page - 1}")
|
||||
}
|
||||
|
||||
dataButton("\uD83D\uDD04 ${pagination.page}", "${pageCallbackDataQueryPrefix}${pagination.page}")
|
||||
dataButton("▶️", "${pageCallbackDataQueryPrefix}${pagination.page + 1}")
|
||||
}
|
||||
|
||||
val selected = mutableListOf<PostId>()
|
||||
krontab.asFlowWithoutDelays().take(pagination.lastIndexExclusive).collectIndexed { i, dateTime ->
|
||||
val postId = selector.takeOneOrNull(now = dateTime, exclude = selected) ?.also { postId ->
|
||||
if (filters.all { it.check(postId, dateTime) }) {
|
||||
selected.add(postId)
|
||||
} else {
|
||||
return@collectIndexed
|
||||
}
|
||||
}
|
||||
|
||||
val post = postsRepo.getFirstMessageInfo(postId ?: return@collectIndexed)
|
||||
if (i < pagination.firstIndex || post == null) {
|
||||
return@collectIndexed
|
||||
}
|
||||
|
||||
row {
|
||||
urlButton(
|
||||
dateTime.local.format(dateTimeFormat),
|
||||
makeLinkToMessage(post.chatId, post.messageId)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCommand("autoschedule_panel", initialFilter = { chatConfig.allSourceChatIds.any { chatId -> it.sameChat(chatId) } }) {
|
||||
val keyboard = buildPage()
|
||||
|
||||
runCatchingSafely {
|
||||
edit(it, replyMarkup = keyboard) {
|
||||
+"Your schedule:"
|
||||
}
|
||||
}.onFailure { _ ->
|
||||
send(it.chat, replyMarkup = keyboard) {
|
||||
+"Your schedule:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMessageDataCallbackQuery(
|
||||
Regex("^$pageCallbackDataQueryPrefix\\d+"),
|
||||
initialFilter = { chatConfig.allSourceChatIds.any { sourceChatId -> it.message.sameChat(sourceChatId) } }
|
||||
) {
|
||||
val page = it.data.removePrefix(pageCallbackDataQueryPrefix).toIntOrNull() ?: let { _ ->
|
||||
answer(it)
|
||||
return@onMessageDataCallbackQuery
|
||||
}
|
||||
|
||||
runCatchingSafely {
|
||||
edit(
|
||||
it.message,
|
||||
replyMarkup = buildPage(Pagination(page, size = pageCallbackDataQuerySize))
|
||||
)
|
||||
}.onFailure { _ ->
|
||||
answer(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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 korlibs.time.DateFormat
|
||||
import korlibs.time.DateTime
|
||||
import korlibs.time.DateTimeTz
|
||||
import korlibs.time.Month
|
||||
import korlibs.time.Year
|
||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||
import dev.inmo.micro_utils.repos.unset
|
||||
import dev.inmo.plaguposter.common.SuccessfulSymbol
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.inmo.plaguposter.triggers.timer
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import com.soywiz.klock.minutes
|
||||
import korlibs.time.DateTime
|
||||
import korlibs.time.minutes
|
||||
|
||||
fun nearestAvailableTimerTime() = (DateTime.now() + 1.minutes).copyDayOfMonth(
|
||||
milliseconds = 0,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package dev.inmo.plaguposter.triggers.timer
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.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 korlibs.time.millisecondsLong
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.plaguposter.triggers.timer
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.micro_utils.repos.KeyValueRepo
|
||||
import dev.inmo.plaguposter.posts.models.PostId
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.plaguposter.triggers.timer
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.micro_utils.coroutines.runCatchingSafely
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.koin.singleWithRandomQualifierAndBinds
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.inmo.plaguposter.triggers.timer.repo
|
||||
|
||||
import com.soywiz.klock.DateTime
|
||||
import korlibs.time.DateTime
|
||||
import dev.inmo.micro_utils.common.firstNotNull
|
||||
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
|
||||
import dev.inmo.micro_utils.pagination.paginate
|
||||
|
||||
Reference in New Issue
Block a user