63 Commits
0.2.3 ... 0.8.0

Author SHA1 Message Date
aeb3bf1f3d ratings auto clearup 2024-11-06 20:48:21 +06:00
569f15330c fixes after update 2024-11-06 20:04:54 +06:00
defc83740d update dependencies 2024-09-22 18:53:55 +06:00
66b2b21c1c start 0.8.0 2024-09-22 18:43:21 +06:00
63800ce19c Merge pull request #26 from InsanusMokrassar/0.7.0
0.7.0
2024-09-22 18:42:43 +06:00
c2612ec6e9 Revert "rework to use just Java target"
This reverts commit 0273284023.
2024-09-07 22:58:56 +06:00
0273284023 rework to use just Java target 2024-09-07 22:55:33 +06:00
9af72d5d0f fix build 2024-09-07 22:42:13 +06:00
b879426f9d Update mppJsProject.gradle 2024-08-13 02:11:33 +06:00
18b8ce0d3d Update mppProjectWithSerialization.gradle 2024-08-13 02:10:58 +06:00
a4686a56df update dependencies 2024-08-13 00:08:21 +06:00
e567f5b3b4 update dependencies 2024-07-13 21:39:37 +06:00
3941c1dfed start 0.7.0 2024-07-13 21:28:37 +06:00
0bdd965a4a Merge pull request #25 from InsanusMokrassar/0.6.0
0.6.0
2024-07-10 22:08:34 +06:00
ad967c002a update github ci/cd scripts 2024-06-28 13:28:12 +06:00
e3958aff3f update dependencies 2024-06-28 13:24:13 +06:00
ae6993efba start 0.6.0 2024-06-28 13:09:37 +06:00
991b0ef3d3 improvements in cachedratingsrepo 2024-05-13 21:42:57 +06:00
aa6c62d66e fixes in CachedRatingsRepo 2024-05-13 21:25:13 +06:00
e6a0d67444 improve of autoclear 2024-05-13 19:57:12 +06:00
16bd62da51 Merge pull request #24 from InsanusMokrassar/0.5.6
0.5.6
2024-05-13 19:43:19 +06:00
edc75a09ee add opportunity to trigger autoclean 2024-05-13 19:41:17 +06:00
0f958f94cb fix in plugin of ratings gc 2024-05-13 19:36:09 +06:00
69320a3b62 start 0.5.6 2024-05-13 19:34:10 +06:00
b6fa2a6cd3 Merge pull request #23 from InsanusMokrassar/0.5.5
0.5.5
2024-04-24 12:15:59 +06:00
e24237beb3 update dependencies 2024-04-24 12:14:33 +06:00
d2d3665c5e start 0.5.5 2024-04-24 11:58:27 +06:00
9ef9de2b8a Merge pull request #22 from InsanusMokrassar/0.5.4
0.5.4
2024-02-18 22:45:21 +06:00
cef350667b update dependencies 2024-02-18 22:41:33 +06:00
80ed679241 start 0.5.4 2024-02-18 21:26:02 +06:00
39391636ac Merge pull request #20 from InsanusMokrassar/several_sources
Several sources
2024-02-15 20:38:17 +06:00
29a19df7fc update dependencies and version 2024-02-15 20:30:10 +06:00
250f88e2fe preview adding of several sources chats 2024-02-15 20:21:48 +06:00
bb433a6441 Merge pull request #21 from InsanusMokrassar/0.5.2
0.5.2
2023-12-11 00:21:28 +06:00
7a8166153f update dependencies 2023-12-11 00:01:05 +06:00
114add0391 start 0.5.2 2023-12-10 23:58:00 +06:00
58b1f26502 0.5.1 2023-11-12 21:57:21 +06:00
ba3d054f0f Update docker-compose.yml 2023-11-09 01:55:24 +06:00
eef2bfce14 add support of inline messages with data callback query in common posts gc 2023-11-06 22:02:54 +06:00
fe96101631 change check of yes/no in checking of messages 2023-11-06 21:56:37 +06:00
7abb6efba3 add force posts check shortcut in inline mode 2023-11-06 21:48:03 +06:00
2f0a823f7c Merge pull request #19 from InsanusMokrassar/0.5.0
0.5.0
2023-11-06 21:31:25 +06:00
730e3c50e9 complete adding of common posts gc 2023-11-06 21:27:43 +06:00
0cc0510876 add retryOnPostFailureTimes 2023-11-06 19:18:19 +06:00
947bd7c2c4 fix build 2023-11-06 18:59:17 +06:00
7f54e86962 update github workflows 2023-11-06 18:52:27 +06:00
db419165a7 fix in publish.gradle 2023-11-06 18:48:43 +06:00
a5b0f429a0 fix settings.gradle 2023-11-06 17:57:21 +06:00
9c161b6dab update dependencies 2023-11-06 17:56:47 +06:00
f6067bb096 start 0.5.0 2023-11-06 17:54:13 +06:00
248740f246 update samples 2023-11-02 22:06:48 +06:00
3ae3cabd80 update sample folder 2023-10-31 20:31:47 +06:00
5fd4042fe3 Merge pull request #18 from InsanusMokrassar/0.4.0
0.4.0
2023-10-31 20:30:00 +06:00
6df4546b81 update sample files 2023-10-31 20:29:06 +06:00
12635c654a update sample files 2023-10-31 20:28:19 +06:00
15bd013eaa Update libs.versions.toml 2023-09-30 07:28:37 +06:00
39b607c4e7 Update gradle.properties 2023-09-26 16:11:33 +06:00
98f3e2a461 Update libs.versions.toml 2023-09-26 16:11:06 +06:00
0d31d90efd Merge pull request #17 from InsanusMokrassar/0.3.0
0.3.0
2023-08-21 11:44:07 +06:00
0ce202a5f6 fill changelog 2023-08-20 15:59:05 +06:00
077f8c30a6 update dependencies 2023-08-20 15:57:10 +06:00
1e9559a2c9 Update config.json 2023-08-13 00:11:04 +06:00
feef8efee1 Merge pull request #16 from InsanusMokrassar/0.2.3
0.2.3
2023-08-13 00:04:27 +06:00
61 changed files with 746 additions and 447 deletions

View File

@@ -8,12 +8,13 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK 17
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 11 java-version: 17
- name: Rewrite version - name: Rewrite version
run: | run: |
printf "\norg.gradle.jvmargs=-Xmx1g -Xms500m\nkotlin.daemon.jvmargs=-Xmx1g -Xms500m\norg.gradle.daemon=false" >> gradle.properties
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" 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 cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
rm gradle.properties rm gradle.properties

View File

@@ -11,8 +11,12 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 17
- name: Rewrite version - name: Rewrite version
run: | run: |
printf "\norg.gradle.jvmargs=-Xmx1g -Xms500m\nkotlin.daemon.jvmargs=-Xmx1g -Xms500m\norg.gradle.daemon=false" >> gradle.properties
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
if [[ "$branch" != "master" ]]; then if [[ "$branch" != "master" ]]; then
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea .idea
.kotlin/
out/* out/*
*.iml *.iml
target target

View File

@@ -1,5 +1,57 @@
# PlaguPoster # PlaguPoster
## 0.8.0
* Dependencies update
* `Ratings`:
* Add autoclearing of ratings without target posts each half hours
## 0.7.0
* Dependencies update
## 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 ## 0.2.3
* Add opportunity to use several target chat ids * Add opportunity to use several target chat ids

View File

@@ -18,7 +18,7 @@ allprojects {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
google() google()
maven { url "https://git.inmo.dev/api/packages/InsanusMokrassar/maven" } maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
} }
} }

View File

@@ -12,15 +12,20 @@ data class ChatConfig(
val targetChatId: IdChatIdentifier? = null, val targetChatId: IdChatIdentifier? = null,
@SerialName("sourceChat") @SerialName("sourceChat")
@Serializable(FullChatIdentifierSerializer::class) @Serializable(FullChatIdentifierSerializer::class)
val sourceChatId: IdChatIdentifier, val sourceChatId: IdChatIdentifier?,
@SerialName("cacheChat") @SerialName("cacheChat")
@Serializable(FullChatIdentifierSerializer::class) @Serializable(FullChatIdentifierSerializer::class)
val cacheChatId: IdChatIdentifier, val cacheChatId: IdChatIdentifier,
@SerialName("targetChats") @SerialName("targetChats")
val targetChatIds: List<@Serializable(FullChatIdentifierSerializer::class) IdChatIdentifier> = emptyList(), val targetChatIds: List<@Serializable(FullChatIdentifierSerializer::class) IdChatIdentifier> = emptyList(),
@SerialName("sourceChats")
val sourceChatIds: List<@Serializable(FullChatIdentifierSerializer::class) IdChatIdentifier> = emptyList(),
) { ) {
val allTargetChatIds by lazy { val allTargetChatIds by lazy {
listOfNotNull(targetChatId) + targetChatIds (listOfNotNull(targetChatId) + targetChatIds).toSet()
}
val allSourceChatIds by lazy {
(listOfNotNull(sourceChatId) + sourceChatIds).toSet()
} }
init { init {
@@ -30,8 +35,8 @@ data class ChatConfig(
} }
fun check(chatId: IdChatIdentifier) = when (chatId) { fun check(chatId: IdChatIdentifier) = when (chatId) {
targetChatId, in allTargetChatIds,
sourceChatId, in allSourceChatIds,
cacheChatId -> true cacheChatId -> true
else -> false else -> false
} }

View File

@@ -1,9 +1,6 @@
package dev.inmo.plaguposter.common package dev.inmo.plaguposter.common
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.message.abstracts.Message import dev.inmo.tgbotapi.types.message.abstracts.Message
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -11,7 +8,7 @@ import kotlinx.serialization.Serializable
data class ShortMessageInfo( data class ShortMessageInfo(
@Serializable(FullChatIdentifierSerializer::class) @Serializable(FullChatIdentifierSerializer::class)
val chatId: IdChatIdentifier, val chatId: IdChatIdentifier,
val messageId: MessageIdentifier val messageId: MessageId
) )
fun Message.short() = ShortMessageInfo(chat.id, messageId) fun Message.short() = ShortMessageInfo(chat.id, messageId)

View File

@@ -11,15 +11,14 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.booleanOrNull
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module import org.koin.core.module.Module
object CommonPlugin : Plugin { object CommonPlugin : Plugin {
private val Log = logger private val Log = logger
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(config: JsonObject) {
single { CoroutineScope(Dispatchers.Default + SupervisorJob()) } single { CoroutineScope(Dispatchers.Default + SupervisorJob()) }
val useCache = (params["useCache"] as? JsonPrimitive) ?.booleanOrNull ?: true val useCache = (config["useCache"] as? JsonPrimitive) ?.booleanOrNull ?: true
useCache(useCache) useCache(useCache)
} }
@@ -27,7 +26,7 @@ object CommonPlugin : Plugin {
val config = koin.get<ChatConfig>() val config = koin.get<ChatConfig>()
Log.iS { "Target chats info: ${config.allTargetChatIds.map { getChat(it) }.joinToString()}" } Log.iS { "Target chats info: ${config.allTargetChatIds.map { getChat(it) }.joinToString()}" }
Log.iS { "Source chat info: ${getChat(config.sourceChatId)}" } Log.iS { "Source chats info: ${config.allSourceChatIds.map { getChat(it) }.joinToString()}" }
Log.iS { "Cache chat info: ${getChat(config.cacheChatId)}" } Log.iS { "Cache chat info: ${getChat(config.cacheChatId)}" }
} }
} }

View File

@@ -1,5 +1,4 @@
kotlin.code.style=official kotlin.code.style=official
org.gradle.jvmargs=-Xmx1024m
org.gradle.parallel=true org.gradle.parallel=true
kotlin.js.generate.externals=true kotlin.js.generate.externals=true
kotlin.incremental=true kotlin.incremental=true
@@ -10,4 +9,4 @@ android.enableJetifier=true
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.2.3 version=0.8.0

View File

@@ -1,16 +1,16 @@
[versions] [versions]
kotlin = "1.8.22" kotlin = "2.0.21"
kotlin-serialization = "1.5.1" kotlin-serialization = "1.7.3"
plagubot = "7.0.0" plagubot = "10.1.0"
tgbotapi = "9.0.0" tgbotapi = "20.0.0"
microutils = "0.19.9" microutils = "0.23.0"
kslog = "1.1.2" kslog = "1.3.6"
krontab = "2.1.2" krontab = "2.6.1"
plagubot-plugins = "0.13.0" plagubot-plugins = "0.24.0"
dokka = "1.8.20" dokka = "1.9.20"
psql = "42.6.0" psql = "42.6.0"

View File

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

View File

@@ -14,7 +14,7 @@ private val actualPlugin = dev.inmo.plagubot.plugins.inline.queries.Plugin
object Plugin : Plugin by actualPlugin { object Plugin : Plugin by actualPlugin {
private val log = TagLogger("InlinePlugin") private val log = TagLogger("InlinePlugin")
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(params: JsonObject) {
single { actualPlugin } single { actualPlugin }
} }

View File

@@ -7,7 +7,7 @@ kotlin {
jvm { jvm {
compilations.main { compilations.main {
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "17"
} }
} }
} }
@@ -34,6 +34,6 @@ kotlin {
} }
java { java {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_17
} }

View File

@@ -25,7 +25,6 @@ kotlin {
jsTest { jsTest {
dependencies { dependencies {
implementation libs.kotlin.test.js implementation libs.kotlin.test.js
implementation libs.kotlin.test.junit
} }
} }
} }

View File

@@ -4,7 +4,13 @@ project.group = "$group"
apply from: "$publishGradlePath" apply from: "$publishGradlePath"
kotlin { kotlin {
jvm() jvm {
compilations.main {
kotlinOptions {
jvmTarget = "17"
}
}
}
js (IR) { js (IR) {
browser() browser()
nodejs() nodejs()
@@ -31,10 +37,14 @@ kotlin {
jsTest { jsTest {
dependencies { dependencies {
implementation libs.kotlin.test.js implementation libs.kotlin.test.js
implementation libs.kotlin.test.junit
} }
} }
} }
} }
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

24
posts/gc/build.gradle Normal file
View File

@@ -0,0 +1,24 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
}
apply from: "$mppProjectWithSerializationPresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":plaguposter.common")
api project(":plaguposter.posts")
api libs.microutils.koin
api libs.krontab
}
}
jvmMain {
dependencies {
api libs.plagubot.plugins.inline.queries
}
}
}
}

View File

@@ -0,0 +1 @@
package dev.inmo.plaguposter.posts.gc

View File

@@ -0,0 +1,192 @@
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.plagubot.registerConfig
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.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(config: JsonObject) {
registerConfig<Config>("messagesChecker") { null }
}
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"
)
)
}
}

View File

@@ -4,15 +4,13 @@ import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.coroutines.runCatchingSafely import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.getAllDistinct 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.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.cached import dev.inmo.micro_utils.repos.cache.cached
import dev.inmo.micro_utils.repos.cache.full.cached import dev.inmo.micro_utils.repos.cache.full.cached
import dev.inmo.micro_utils.repos.deleteById import dev.inmo.micro_utils.repos.cache.full.fullyCached
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.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.registerConfig
import dev.inmo.plaguposter.common.ChatConfig import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.common.UnsuccessfulSymbol import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.common.useCache import dev.inmo.plaguposter.common.useCache
@@ -33,7 +31,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.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.types.IdChatIdentifier 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.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.types.message.ParseMode import dev.inmo.tgbotapi.types.message.ParseMode
@@ -57,10 +56,8 @@ object Plugin : Plugin {
val rootButtonText: String = "◀️", val rootButtonText: String = "◀️",
val refreshButtonText: String? = "\uD83D\uDD04" val refreshButtonText: String? = "\uD83D\uDD04"
) )
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(params: JsonObject) {
params["panel"] ?.let { element -> registerConfig<Config>("panel") { null }
single { get<Json>().decodeFromJsonElement(Config.serializer(), element) }
}
single { single {
val config = getOrNull<Config>() ?: Config() val config = getOrNull<Config>() ?: Config()
val builtInButtons = listOfNotNull( val builtInButtons = listOfNotNull(
@@ -99,7 +96,7 @@ object Plugin : Plugin {
val api = koin.get<PanelButtonsAPI>() val api = koin.get<PanelButtonsAPI>()
val basePostsMessages = PostsMessages(koin.get(), koin.get()) val basePostsMessages = PostsMessages(koin.get(), koin.get())
val postsMessages = if (koin.useCache) { val postsMessages = if (koin.useCache) {
basePostsMessages.cached(FullKVCache(), koin.get()) basePostsMessages.fullyCached(MapKeyValueRepo(), koin.get())
} else { } else {
basePostsMessages basePostsMessages
} }
@@ -115,7 +112,7 @@ object Plugin : Plugin {
firstContent.chatId, firstContent.chatId,
text = config.text, text = config.text,
parseMode = config.parseMode, parseMode = config.parseMode,
replyToMessageId = firstContent.messageId, replyParameters = ReplyParameters(firstContent.chatId, firstContent.messageId),
replyMarkup = InlineKeyboardMarkup(buttons), replyMarkup = InlineKeyboardMarkup(buttons),
disableNotification = true disableNotification = true
).also { sentMessage -> ).also { sentMessage ->
@@ -131,7 +128,7 @@ object Plugin : Plugin {
suspend fun refreshPostMessage( suspend fun refreshPostMessage(
postId: PostId, postId: PostId,
chatId: IdChatIdentifier, chatId: IdChatIdentifier,
messageId: MessageIdentifier messageId: MessageId
) { ) {
val post = postsRepo.getById(postId) ?: return val post = postsRepo.getById(postId) ?: return
val buttons = api.buttonsBuilders.chunked(config.buttonsPerRow).mapNotNull { row -> val buttons = api.buttonsBuilders.chunked(config.buttonsPerRow).mapNotNull { row ->
@@ -149,7 +146,7 @@ object Plugin : Plugin {
onMessageDataCallbackQuery ( onMessageDataCallbackQuery (
initialFilter = { 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) val postId = it.data.removePrefix(PanelButtonsAPI.openGlobalMenuDataPrefix).let(::PostId)
@@ -158,7 +155,7 @@ object Plugin : Plugin {
} }
onMessageDataCallbackQuery( onMessageDataCallbackQuery(
initialFilter = { initialFilter = {
it.data.startsWith("delete ") && it.message.chat.id == chatsConfig.sourceChatId it.data.startsWith("delete ") && it.message.chat.id in chatsConfig.allSourceChatIds
} }
) { query -> ) { query ->
val postId = query.data.removePrefix("delete ").let(::PostId) val postId = query.data.removePrefix("delete ").let(::PostId)
@@ -185,7 +182,7 @@ object Plugin : Plugin {
} }
onMessageDataCallbackQuery( onMessageDataCallbackQuery(
initialFilter = { initialFilter = {
it.data.startsWith("refresh ") && it.message.chat.id == chatsConfig.sourceChatId it.data.startsWith("refresh ") && it.message.chat.id in chatsConfig.allSourceChatIds
} }
) { query -> ) { query ->
val postId = query.data.removePrefix("refresh ").let(::PostId) val postId = query.data.removePrefix("refresh ").let(::PostId)

View File

@@ -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.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.micro_utils.repos.mappers.withMapper import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import kotlinx.serialization.builtins.PairSerializer import kotlinx.serialization.builtins.PairSerializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
private val ChatIdToMessageSerializer = PairSerializer(FullChatIdentifierSerializer, MessageIdentifier.serializer()) private val ChatIdToMessageSerializer = PairSerializer(FullChatIdentifierSerializer, MessageId.serializer())
fun PostsMessages( fun PostsMessages(
database: Database, database: Database,
json: Json json: Json
): KeyValueRepo<PostId, Pair<IdChatIdentifier, MessageIdentifier>> = ExposedKeyValueRepo<String, String>( ): KeyValueRepo<PostId, Pair<IdChatIdentifier, MessageId>> = ExposedKeyValueRepo<String, String>(
database, database,
{ text("postId") }, { text("postId") },
{ text("chatToMessage") }, { text("chatToMessage") },

View File

@@ -1,10 +1,7 @@
package dev.inmo.plaguposter.posts.models package dev.inmo.plaguposter.posts.models
import dev.inmo.tgbotapi.extensions.utils.possiblyMediaGroupMessageOrNull import dev.inmo.tgbotapi.extensions.utils.possiblyMediaGroupMessageOrNull
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -13,8 +10,8 @@ import kotlinx.serialization.Serializable
data class PostContentInfo( data class PostContentInfo(
@Serializable(FullChatIdentifierSerializer::class) @Serializable(FullChatIdentifierSerializer::class)
val chatId: IdChatIdentifier, val chatId: IdChatIdentifier,
val messageId: MessageIdentifier, val messageId: MessageId,
val group: String?, val group: MediaGroupId?,
val order: Int val order: Int
) { ) {
companion object { companion object {

View File

@@ -5,10 +5,10 @@ import dev.inmo.micro_utils.repos.ReadCRUDRepo
import dev.inmo.plaguposter.posts.models.* import dev.inmo.plaguposter.posts.models.*
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier import dev.inmo.tgbotapi.types.MessageId
interface ReadPostsRepo : ReadCRUDRepo<RegisteredPost, PostId> { 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 getPostCreationTime(postId: PostId): DateTime?
suspend fun getFirstMessageInfo(postId: PostId): PostContentInfo? suspend fun getFirstMessageInfo(postId: PostId): PostContentInfo?
} }

View File

@@ -11,7 +11,6 @@ import dev.inmo.tgbotapi.extensions.api.send.copyMessage
import dev.inmo.tgbotapi.extensions.api.send.send import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.utils.* import dev.inmo.tgbotapi.extensions.utils.*
import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
import dev.inmo.tgbotapi.types.message.content.MediaGroupPartContent import dev.inmo.tgbotapi.types.message.content.MediaGroupPartContent
class PostPublisher( class PostPublisher(
@@ -21,10 +20,10 @@ class PostPublisher(
private val targetChatIds: List<IdChatIdentifier>, private val targetChatIds: List<IdChatIdentifier>,
private val deleteAfterPosting: Boolean = true private val deleteAfterPosting: Boolean = true
) { ) {
suspend fun publish(postId: PostId) { suspend fun publish(postId: PostId): Boolean {
val messagesInfo = postsRepo.getById(postId) ?: let { val messagesInfo = postsRepo.getById(postId) ?: let {
logger.w { "Unable to get post with id $postId for publishing" } logger.w { "Unable to get post with id $postId for publishing" }
return return false
} }
val sortedMessagesContents = messagesInfo.content.groupBy { it.group }.flatMap { (group, list) -> val sortedMessagesContents = messagesInfo.content.groupBy { it.group }.flatMap { (group, list) ->
if (group == null) { if (group == null) {
@@ -35,22 +34,26 @@ class PostPublisher(
listOf(list.first().order to list) listOf(list.first().order to list)
} }
}.sortedBy { it.first } }.sortedBy { it.first }
var haveSentMessages = false
sortedMessagesContents.forEach { (_, contents) -> sortedMessagesContents.forEach { (_, contents) ->
contents.singleOrNull() ?.also { contents.singleOrNull() ?.also {
targetChatIds.forEach { targetChatId -> targetChatIds.forEach { targetChatId ->
runCatching { runCatching {
bot.copyMessage(targetChatId, it.chatId, it.messageId) bot.copyMessage(fromChatId = it.chatId, messageId = it.messageId, toChatId = targetChatId)
}.onFailure { _ -> }.onFailure { _ ->
runCatching { runCatching {
bot.forwardMessage( bot.forwardMessage(
it.chatId, fromChatId = it.chatId,
targetChatId, messageId = it.messageId,
it.messageId toChatId = cachingChatId
) )
}.onSuccess { }.onSuccess {
bot.copyMessage(targetChatId, it) bot.copyMessage(targetChatId, it)
haveSentMessages = true
} }
}.onSuccess {
haveSentMessages = true
} }
} }
return@forEach return@forEach
@@ -61,12 +64,14 @@ class PostPublisher(
forwardedMessage.withContentOrNull<MediaGroupPartContent>() ?: null.also { _ -> forwardedMessage.withContentOrNull<MediaGroupPartContent>() ?: null.also { _ ->
targetChatIds.forEach { targetChatId -> targetChatIds.forEach { targetChatId ->
bot.copyMessage(targetChatId, forwardedMessage) bot.copyMessage(targetChatId, forwardedMessage)
haveSentMessages = true
} }
} }
} }
resultContents.singleOrNull() ?.also { resultContents.singleOrNull() ?.also {
targetChatIds.forEach { targetChatId -> targetChatIds.forEach { targetChatId ->
bot.copyMessage(targetChatId, it) bot.copyMessage(targetChatId, it)
haveSentMessages = true
} }
return@forEach return@forEach
} ?: resultContents.chunked(mediaCountInMediaGroup.last).forEach { } ?: resultContents.chunked(mediaCountInMediaGroup.last).forEach {
@@ -75,6 +80,7 @@ class PostPublisher(
targetChatId, targetChatId,
it.map { it.content.toMediaGroupMemberTelegramMedia() } it.map { it.content.toMediaGroupMemberTelegramMedia() }
) )
haveSentMessages = true
} }
} }
} }
@@ -83,5 +89,6 @@ class PostPublisher(
postsRepo.deleteById(postId) postsRepo.deleteById(postId)
} }
return haveSentMessages
} }
} }

View File

@@ -7,6 +7,7 @@ import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithBinds import dev.inmo.micro_utils.koin.singleWithBinds
import dev.inmo.micro_utils.repos.deleteById import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.database
import dev.inmo.plaguposter.common.SuccessfulSymbol import dev.inmo.plaguposter.common.SuccessfulSymbol
import dev.inmo.plaguposter.common.UnsuccessfulSymbol import dev.inmo.plaguposter.common.UnsuccessfulSymbol
import dev.inmo.plaguposter.posts.exposed.ExposedPostsRepo import dev.inmo.plaguposter.posts.exposed.ExposedPostsRepo
@@ -26,7 +27,6 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onComman
import dev.inmo.tgbotapi.types.message.textsources.regular import dev.inmo.tgbotapi.types.message.textsources.regular
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module import org.koin.core.module.Module
@@ -37,8 +37,8 @@ object Plugin : Plugin {
val autoRemoveMessages: Boolean = true, val autoRemoveMessages: Boolean = true,
val deleteAfterPublishing: Boolean = true val deleteAfterPublishing: Boolean = true
) )
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(config: JsonObject) {
val configJson = params["posts"] ?: this@Plugin.let { val configJson = config["posts"] ?: this@Plugin.let {
it.logger.w { it.logger.w {
"Unable to load posts plugin due to absence of `posts` key in config" "Unable to load posts plugin due to absence of `posts` key in config"
} }
@@ -58,7 +58,7 @@ object Plugin : Plugin {
} }
single { single {
val config = get<Config>() val config = get<Config>()
PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.allTargetChatIds, config.deleteAfterPublishing) PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.allTargetChatIds.toList(), config.deleteAfterPublishing)
} }
} }

View File

@@ -1,10 +1,13 @@
package dev.inmo.plaguposter.posts.cached package dev.inmo.plaguposter.posts.cached
import dev.inmo.micro_utils.coroutines.SmartRWLocker
import korlibs.time.DateTime import korlibs.time.DateTime
import dev.inmo.micro_utils.pagination.FirstPagePagination import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging
import dev.inmo.micro_utils.repos.CRUDRepo 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.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.full.FullCRUDCacheRepo import dev.inmo.micro_utils.repos.cache.full.FullCRUDCacheRepo
import dev.inmo.plaguposter.posts.models.NewPost import dev.inmo.plaguposter.posts.models.NewPost
@@ -13,24 +16,25 @@ import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.models.RegisteredPost import dev.inmo.plaguposter.posts.models.RegisteredPost
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.tgbotapi.types.IdChatIdentifier 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.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class CachedPostsRepo( class CachedPostsRepo(
private val parentRepo: PostsRepo, private val parentRepo: PostsRepo,
private val scope: CoroutineScope, 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( ) : PostsRepo, CRUDRepo<RegisteredPost, PostId, NewPost> by FullCRUDCacheRepo(
parentRepo, parentRepo,
kvCache, kvCache,
scope, scope,
skipStartInvalidate = false, skipStartInvalidate = false,
locker = SmartRWLocker(),
{ it.id } { it.id }
) { ) {
override val removedPostsFlow: Flow<RegisteredPost> by parentRepo::removedPostsFlow 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) { doForAllWithNextPaging(firstPageWithOneElementPagination) {
kvCache.values(it).also { kvCache.values(it).also {
it.results.forEach { it.results.forEach {

View File

@@ -4,8 +4,7 @@ import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.repos.KeyValuesRepo import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.exposed.* import dev.inmo.micro_utils.repos.exposed.*
import dev.inmo.plaguposter.posts.models.* import dev.inmo.plaguposter.posts.models.*
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.IdChatIdentifier
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
internal class ExposedContentInfoRepo( internal class ExposedContentInfoRepo(
@@ -21,9 +20,9 @@ internal class ExposedContentInfoRepo(
val ResultRow.asObject val ResultRow.asObject
get() = PostContentInfo( get() = PostContentInfo(
IdChatIdentifier(get(chatIdColumn), get(threadIdColumn)), IdChatIdentifier(RawChatId(get(chatIdColumn)), get(threadIdColumn) ?.let(::MessageThreadId)),
get(messageIdColumn), MessageId(get(messageIdColumn)),
get(groupColumn), get(groupColumn) ?.let(::MediaGroupId),
get(orderColumn) get(orderColumn)
) )

View File

@@ -10,7 +10,7 @@ import dev.inmo.plaguposter.posts.models.*
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier import dev.inmo.tgbotapi.types.MessageId
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
@@ -26,6 +26,7 @@ class ExposedPostsRepo(
) { ) {
val idColumn = text("id") val idColumn = text("id")
val createdColumn = double("datetime").default(0.0) val createdColumn = double("datetime").default(0.0)
val latestUpdateColumn = double("latest_update").default(0.0)
private val contentRepo by lazy { private val contentRepo by lazy {
ExposedContentInfoRepo( ExposedContentInfoRepo(
@@ -47,7 +48,7 @@ class ExposedPostsRepo(
id, id,
DateTime(get(createdColumn)), DateTime(get(createdColumn)),
with(contentRepo) { with(contentRepo) {
select { postIdColumn.eq(id.string) }.map { selectAll().where { postIdColumn.eq(id.string) }.map {
it.asObject it.asObject
} }
} }
@@ -68,20 +69,22 @@ class ExposedPostsRepo(
id, id,
DateTime(get(createdColumn)), DateTime(get(createdColumn)),
with(contentRepo) { with(contentRepo) {
select { postIdColumn.eq(id.string) }.map { selectAll().where { postIdColumn.eq(id.string) }.map {
it.asObject it.asObject
} }
} }
) )
} }
override fun createAndInsertId(value: NewPost, it: InsertStatement<Number>): PostId { override fun createAndInsertId(value: NewPost, it: UpdateBuilder<Int>): PostId {
val id = PostId(uuid4().toString()) val id = PostId(uuid4().toString())
it[idColumn] = id.string it[idColumn] = id.string
return id 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) { private fun updateContent(post: RegisteredPost) {
transaction(database) { transaction(database) {
@@ -90,10 +93,10 @@ class ExposedPostsRepo(
post.content.forEach { contentInfo -> post.content.forEach { contentInfo ->
insert { insert {
it[postIdColumn] = post.id.string it[postIdColumn] = post.id.string
it[chatIdColumn] = contentInfo.chatId.chatId it[chatIdColumn] = contentInfo.chatId.chatId.long
it[threadIdColumn] = contentInfo.chatId.threadId it[threadIdColumn] = contentInfo.chatId.threadId ?.long
it[messageIdColumn] = contentInfo.messageId it[messageIdColumn] = contentInfo.messageId.long
it[groupColumn] = contentInfo.group it[groupColumn] = contentInfo.group ?.string
it[orderColumn] = contentInfo.order it[orderColumn] = contentInfo.order
} }
} }
@@ -101,7 +104,7 @@ class ExposedPostsRepo(
} }
} }
override fun insert(value: NewPost, it: InsertStatement<Number>) { override fun insert(value: NewPost, it: UpdateBuilder<Int>) {
super.insert(value, it) super.insert(value, it)
it[createdColumn] = DateTime.now().unixMillis it[createdColumn] = DateTime.now().unixMillis
} }
@@ -141,7 +144,7 @@ class ExposedPostsRepo(
existsIds existsIds
} else { } else {
existsIds.filter { existsIds.filter {
select { selectById(it) }.limit(1).none() selectAll().where { selectById(it) }.limit(1).none()
} }
} }
}.forEach { }.forEach {
@@ -150,25 +153,25 @@ class ExposedPostsRepo(
} }
} }
override suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageIdentifier): PostId? { override suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageId): PostId? {
return transaction(database) { return transaction(database) {
with(contentRepo) { with(contentRepo) {
select { selectAll().where {
chatIdColumn.eq(chatId.chatId) chatIdColumn.eq(chatId.chatId.long)
.and(chatId.threadId ?.let { threadIdColumn.eq(it) } ?: threadIdColumn.isNull()) .and(chatId.threadId ?.let { threadIdColumn.eq(it.long) } ?: threadIdColumn.isNull())
.and(messageIdColumn.eq(messageId)) .and(messageIdColumn.eq(messageId.long))
}.limit(1).firstOrNull() ?.get(postIdColumn) }.limit(1).firstOrNull() ?.get(postIdColumn)
} ?.let(::PostId) } ?.let(::PostId)
} }
} }
override suspend fun getPostCreationTime(postId: PostId): DateTime? = transaction(database) { override suspend fun getPostCreationTime(postId: PostId): DateTime? = transaction(database) {
select { selectById(postId) }.limit(1).firstOrNull() ?.get(createdColumn) ?.let(::DateTime) selectAll().where { selectById(postId) }.limit(1).firstOrNull() ?.get(createdColumn) ?.let(::DateTime)
} }
override suspend fun getFirstMessageInfo(postId: PostId): PostContentInfo? = transaction(database) { override suspend fun getFirstMessageInfo(postId: PostId): PostContentInfo? = transaction(database) {
with(contentRepo) { with(contentRepo) {
select { postIdColumn.eq(postId.string) }.limit(1).firstOrNull() ?.asObject selectAll().where { postIdColumn.eq(postId.string) }.limit(1).firstOrNull() ?.asObject
} }
} }
} }

View File

@@ -119,12 +119,12 @@ object Plugin : Plugin {
null null
} }
onCommand("start_post", initialFilter = { it.sameChat(config.sourceChatId) }) { onCommand("start_post", initialFilter = { config.allSourceChatIds.any { chatId -> it.sameChat(chatId) } }) {
startChain(RegistrationState.InProcess(it.chat.id, emptyList())) startChain(RegistrationState.InProcess(it.chat.id, emptyList()))
} }
onContentMessage( onContentMessage(
initialFilter = { it.sameChat(config.sourceChatId) && !FirstSourceIsCommandsFilter(it) } initialFilter = { config.allSourceChatIds.any { chatId -> it.sameChat(chatId) } && !FirstSourceIsCommandsFilter(it) }
) { ) {
startChain(RegistrationState.Finish(it.chat.id, PostContentInfo.fromMessage(it))) startChain(RegistrationState.Finish(it.chat.id, PostContentInfo.fromMessage(it)))
} }

View File

@@ -1,7 +1,7 @@
apply plugin: 'maven-publish' apply plugin: 'maven-publish'
task javadocsJar(type: Jar) { task javadocsJar(type: Jar) {
classifier = 'javadoc' archiveClassifier = 'javadoc'
} }
publishing { publishing {
@@ -79,4 +79,27 @@ if (project.hasProperty("signing.gnupg.keyName")) {
dependsOn(it) dependsOn(it)
} }
} }
// Workaround to make android sign operations depend on signing tasks
project.getTasks().withType(AbstractPublishToMaven.class).configureEach {
def signingTasks = project.getTasks().withType(Sign.class)
mustRunAfter(signingTasks)
}
// Workaround to make test tasks use sign
project.getTasks().withType(Sign.class).configureEach { signTask ->
def withoutSign = (signTask.name.startsWith("sign") ? signTask.name.minus("sign") : signTask.name)
def pubName = withoutSign.endsWith("Publication") ? withoutSign.substring(0, withoutSign.length() - "Publication".length()) : withoutSign
// These tasks only exist for native targets, hence findByName() to avoid trying to find them for other targets
// Task ':linkDebugTest<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def debugTestTask = tasks.findByName("linkDebugTest$pubName")
if (debugTestTask != null) {
signTask.mustRunAfter(debugTestTask)
}
// Task ':compileTestKotlin<platform>' uses this output of task ':sign<platform>Publication' without declaring an explicit or implicit dependency
def testTask = tasks.findByName("compileTestKotlin$pubName")
if (testTask != null) {
signTask.mustRunAfter(testTask)
}
}
} }

View File

@@ -11,6 +11,7 @@ kotlin {
dependencies { dependencies {
api project(":plaguposter.common") api project(":plaguposter.common")
api project(":plaguposter.posts") api project(":plaguposter.posts")
api libs.krontab
} }
} }
} }

View File

@@ -5,15 +5,23 @@ import korlibs.time.seconds
import dev.inmo.krontab.KrontabTemplate import dev.inmo.krontab.KrontabTemplate
import dev.inmo.krontab.toSchedule import dev.inmo.krontab.toSchedule
import dev.inmo.krontab.utils.asFlowWithDelays 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.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithRandomQualifier
import dev.inmo.micro_utils.repos.* import dev.inmo.micro_utils.repos.*
import dev.inmo.plagubot.Plugin 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.plagubot.registerConfig
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.models.Rating import dev.inmo.plaguposter.ratings.models.Rating
import dev.inmo.plaguposter.ratings.repo.RatingsRepo import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.types.Seconds import dev.inmo.tgbotapi.types.Seconds
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
@@ -34,8 +42,8 @@ object Plugin : Plugin {
val skipPostAge: Seconds? = null val skipPostAge: Seconds? = null
) )
} }
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(params: JsonObject) {
single { get<Json>().decodeFromJsonElement(Config.serializer(), params["gc"] ?: return@single null) } registerConfig<Config>("gc") { null }
} }
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
@@ -59,12 +67,23 @@ object Plugin : Plugin {
} }
} }
config.autoclear ?.let { autoclear -> config.autoclear ?.let { autoclear ->
val autoClearLogger = KSLog("autoclear")
suspend fun doAutoClear() { suspend fun doAutoClear() {
autoClearLogger.i { "Start autoclear" }
val dropCreatedBefore = DateTime.now() - (autoclear.skipPostAge ?: 0).seconds val dropCreatedBefore = DateTime.now() - (autoclear.skipPostAge ?: 0).seconds
ratingsRepo.getPostsWithRatingLessEq(autoclear.rating).keys.forEach { autoClearLogger.i { "Posts drop created before: ${dropCreatedBefore.toStringDefault()}" }
if ((postsRepo.getPostCreationTime(it) ?: return@forEach) < dropCreatedBefore) { val idsToDelete = ratingsRepo.getPostsWithRatingLessEq(autoclear.rating).keys.also {
postsRepo.deleteById(it) 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 { runCatchingSafely {
@@ -73,6 +92,13 @@ object Plugin : Plugin {
autoclear.autoClearKrontab.toSchedule().asFlowWithDelays().subscribeSafelyWithoutExceptions(scope) { autoclear.autoClearKrontab.toSchedule().asFlowWithDelays().subscribeSafelyWithoutExceptions(scope) {
doAutoClear() doAutoClear()
} }
onCommand("clean_posts_by_ratings") {
doAutoClear()
}
koin.getOrNull<InlineTemplatesRepo>() ?.addTemplate(
OfferTemplate("Force autoclear", listOf(Format("/clean_posts_by_ratings")))
)
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package dev.inmo.plaguposter.ratings.selector package dev.inmo.plaguposter.ratings.selector
import dev.inmo.micro_utils.repos.KeyValueRepo
import korlibs.time.DateTime import korlibs.time.DateTime
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
@@ -9,13 +10,14 @@ import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
class DefaultSelector ( class DefaultSelector (
private val config: SelectorConfig, private val config: SelectorConfig,
private val ratingsRepo: RatingsRepo, private val ratingsRepo: RatingsRepo,
private val postsRepo: PostsRepo private val postsRepo: PostsRepo,
private val latestChosenRepo: KeyValueRepo<PostId, DateTime>
) : Selector { ) : Selector {
override suspend fun take(n: Int, now: DateTime, exclude: List<PostId>): List<PostId> { override suspend fun take(n: Int, now: DateTime, exclude: List<PostId>): List<PostId> {
val result = mutableListOf<PostId>() val result = mutableListOf<PostId>()
do { do {
val selected = config.active(now.time) ?.rating ?.select(ratingsRepo, postsRepo, result + exclude, now) ?: break val selected = config.active(now.time) ?.rating ?.select(ratingsRepo, postsRepo, result + exclude, now, latestChosenRepo) ?: break
result.add(selected) result.add(selected)
} while (result.size < n) } while (result.size < n)

View File

@@ -5,4 +5,9 @@ import dev.inmo.plaguposter.posts.models.PostId
interface Selector { interface Selector {
suspend fun take(n: Int = 1, now: DateTime = DateTime.now(), exclude: List<PostId> = emptyList()): 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()
} }

View File

@@ -2,11 +2,9 @@ package dev.inmo.plaguposter.ratings.selector.models
import korlibs.time.DateTime import korlibs.time.DateTime
import korlibs.time.seconds import korlibs.time.seconds
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging import dev.inmo.micro_utils.pagination.utils.getAllByWithNextPaging
import dev.inmo.micro_utils.repos.pagination.getAll import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.plaguposter.common.DateTimeSerializer import dev.inmo.micro_utils.repos.unset
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.models.Rating import dev.inmo.plaguposter.ratings.models.Rating
@@ -25,17 +23,23 @@ data class RatingConfig(
val max: Rating? = null, val max: Rating? = null,
val prefer: Prefer = Prefer.Random, val prefer: Prefer = Prefer.Random,
val otherwise: RatingConfig? = null, val otherwise: RatingConfig? = null,
val postAge: Seconds? = null val postAge: Seconds? = null,
val uniqueCount: Int? = null
) { ) {
suspend fun select( suspend fun select(
ratingsRepo: RatingsRepo, ratingsRepo: RatingsRepo,
postsRepo: PostsRepo, postsRepo: PostsRepo,
exclude: List<PostId>, exclude: List<PostId>,
now: DateTime now: DateTime,
latestChosenRepo: KeyValueRepo<PostId, DateTime>
): PostId? { ): PostId? {
var reversed: Boolean = false var reversed: Boolean = false
var count: Int? = null var count: Int? = null
val allowedCreationTime = now - (postAge ?: 0).seconds 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) { when (prefer) {
Prefer.Max -> { Prefer.Max -> {
@@ -59,40 +63,53 @@ data class RatingConfig(
ratingsRepo.getAllByWithNextPaging { keys(it) } ratingsRepo.getAllByWithNextPaging { keys(it) }
} }
else -> { else -> {
ratingsRepo.getPostsWithRatingLessEq(max, exclude = exclude).keys ratingsRepo.getPostsWithRatingLessEq(max, exclude = resultExcluded).keys
} }
} }
} }
else -> { else -> {
when (max) { when (max) {
null -> { null -> {
ratingsRepo.getPostsWithRatingGreaterEq(min, exclude = exclude).keys ratingsRepo.getPostsWithRatingGreaterEq(min, exclude = resultExcluded).keys
} }
else -> { else -> {
ratingsRepo.getPosts(min .. max, reversed, count, exclude = exclude).keys ratingsRepo.getPosts(min .. max, reversed, count, exclude = resultExcluded).keys
} }
} }
} }
}.filter { }.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.Max,
Prefer.Min -> posts.firstOrNull() Prefer.Min -> posts.firstOrNull()
Prefer.Random -> posts.randomOrNull() 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) @Serializable(Prefer.Serializer::class)
sealed interface Prefer { sealed interface Prefer {
val type: String val type: String
@Serializable(Serializer::class) @Serializable(Serializer::class)
object Max : Prefer { override val type: String = "max" } data object Max : Prefer { override val type: String = "max" }
@Serializable(Serializer::class) @Serializable(Serializer::class)
object Min : Prefer { override val type: String = "min" } data object Min : Prefer { override val type: String = "min" }
@Serializable(Serializer::class) @Serializable(Serializer::class)
object Random : Prefer { override val type: String = "random" } data object Random : Prefer { override val type: String = "random" }
object Serializer : KSerializer<Prefer> { object Serializer : KSerializer<Prefer> {
override val descriptor: SerialDescriptor = String.serializer().descriptor override val descriptor: SerialDescriptor = String.serializer().descriptor

View File

@@ -1,14 +1,33 @@
package dev.inmo.plaguposter.ratings.selector 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.plagubot.Plugin
import dev.inmo.plagubot.registerConfig
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
import korlibs.time.DateTime
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.core.qualifier.qualifier
object Plugin : Plugin { object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(config: JsonObject) {
single { get<Json>().decodeFromJsonElement(SelectorConfig.serializer(), params["selector"] ?: return@single null) } registerConfig<SelectorConfig>("selector") { 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"))) }
} }
} }

View File

@@ -122,7 +122,7 @@ suspend fun BehaviourContext.includeRootNavigationButtonsHandler(
edit( edit(
it.message, 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) { if (prefix == RootButtonsShowRatingData) {
runCatchingSafely { runCatchingSafely {
val rating = ratingRaw.toDoubleOrNull() ?: return@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) answer(it)

View File

@@ -1,14 +1,15 @@
package dev.inmo.plaguposter.ratings.source.repos package dev.inmo.plaguposter.ratings.source.repos
import dev.inmo.micro_utils.repos.KeyValueRepo 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.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.full.fullyCached import dev.inmo.micro_utils.repos.cache.full.fullyCached
import dev.inmo.plaguposter.common.ShortMessageInfo import dev.inmo.plaguposter.common.ShortMessageInfo
import dev.inmo.tgbotapi.types.PollIdentifier import dev.inmo.tgbotapi.types.PollId
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
class CachedPollsToMessagesInfoRepo( class CachedPollsToMessagesInfoRepo(
private val repo: PollsToMessagesInfoRepo, private val repo: PollsToMessagesInfoRepo,
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val kvCache: FullKVCache<PollIdentifier, ShortMessageInfo> = FullKVCache() private val kvCache: KeyValueRepo<PollId, ShortMessageInfo> = MapKeyValueRepo()
) : PollsToMessagesInfoRepo, KeyValueRepo<PollIdentifier, ShortMessageInfo> by repo.fullyCached(kvCache, scope) ) : PollsToMessagesInfoRepo, KeyValueRepo<PollId, ShortMessageInfo> by repo.fullyCached(kvCache, scope)

View File

@@ -1,14 +1,15 @@
package dev.inmo.plaguposter.ratings.source.repos package dev.inmo.plaguposter.ratings.source.repos
import dev.inmo.micro_utils.repos.KeyValueRepo 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.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.full.fullyCached import dev.inmo.micro_utils.repos.cache.full.fullyCached
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.types.PollIdentifier import dev.inmo.tgbotapi.types.PollId
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
class CachedPollsToPostsIdsRepo( class CachedPollsToPostsIdsRepo(
private val repo: PollsToPostsIdsRepo, private val repo: PollsToPostsIdsRepo,
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val kvCache: FullKVCache<PollIdentifier, PostId> = FullKVCache() private val kvCache: KeyValueRepo<PollId, PostId> = MapKeyValueRepo()
) : PollsToPostsIdsRepo, KeyValueRepo<PollIdentifier, PostId> by repo.fullyCached(kvCache, scope) ) : PollsToPostsIdsRepo, KeyValueRepo<PollId, PostId> by repo.fullyCached(kvCache, scope)

View File

@@ -3,6 +3,6 @@ package dev.inmo.plaguposter.ratings.source.repos
import dev.inmo.micro_utils.repos.KeyValueRepo import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.plaguposter.common.ShortMessageInfo import dev.inmo.plaguposter.common.ShortMessageInfo
import dev.inmo.plaguposter.posts.models.PostId 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>

View File

@@ -2,6 +2,6 @@ package dev.inmo.plaguposter.ratings.source.repos
import dev.inmo.micro_utils.repos.KeyValueRepo import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.plaguposter.posts.models.PostId 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>

View File

@@ -10,10 +10,12 @@ import dev.inmo.micro_utils.repos.id
import dev.inmo.micro_utils.repos.pagination.getAll import dev.inmo.micro_utils.repos.pagination.getAll
import dev.inmo.micro_utils.repos.set import dev.inmo.micro_utils.repos.set
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.database
import dev.inmo.plaguposter.common.* import dev.inmo.plaguposter.common.*
import dev.inmo.plagubot.plugins.inline.queries.models.Format 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.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plagubot.registerConfig
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder
import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI
@@ -28,6 +30,7 @@ import dev.inmo.plaguposter.ratings.utils.postsByRatings
import dev.inmo.tgbotapi.extensions.api.answers.answer import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.delete import dev.inmo.tgbotapi.extensions.api.delete
import dev.inmo.tgbotapi.extensions.api.edit.edit 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.reply
import dev.inmo.tgbotapi.extensions.api.send.send import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
@@ -37,10 +40,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.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard 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.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.message.textsources.bold import dev.inmo.tgbotapi.types.message.textsources.bold
import dev.inmo.tgbotapi.types.message.textsources.regular 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.buildEntities
import dev.inmo.tgbotapi.utils.extensions.makeSourceString
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -62,10 +69,8 @@ object Plugin : Plugin {
val panelButtonText: String = "Ratings" val panelButtonText: String = "Ratings"
) )
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(params: JsonObject) {
single { registerConfig<Config>("ratingsPolls")
get<Json>().decodeFromJsonElement(Config.serializer(), params["ratingsPolls"] ?: error("Unable to load config for rating polls in $params"))
}
single<RatingsVariants>(ratingVariantsQualifier) { get<Config>().variants } single<RatingsVariants>(ratingVariantsQualifier) { get<Config>().variants }
single { ExposedPollsToPostsIdsRepo(database) } single { ExposedPollsToPostsIdsRepo(database) }
@@ -111,7 +116,7 @@ object Plugin : Plugin {
onPollUpdates (markerFactory = { it.id }) { poll -> onPollUpdates (markerFactory = { it.id }) { poll ->
val postId = pollsToPostsIdsRepo.get(poll.id) ?: return@onPollUpdates val postId = pollsToPostsIdsRepo.get(poll.id) ?: return@onPollUpdates
val newRating = poll.options.sumOf { 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)) ratingsRepo.set(postId, Rating(newRating))
} }
@@ -128,8 +133,10 @@ object Plugin : Plugin {
val sent = send( val sent = send(
content.chatId, content.chatId,
config.ratingOfferText, config.ratingOfferText,
config.variants.keys.toList(), options = config.variants.map {
replyToMessageId = content.messageId InputPollOption(it.key)
},
replyParameters = ReplyParameters(content.chatId, content.messageId)
) )
pollsToPostsIdsRepo.set(sent.content.poll.id, postId) pollsToPostsIdsRepo.set(sent.content.poll.id, postId)
pollsToMessageInfoRepo.set(sent.content.poll.id, sent.short()) pollsToMessageInfoRepo.set(sent.content.poll.id, sent.short())
@@ -242,7 +249,7 @@ object Plugin : Plugin {
} }
} }
onCommand("ratings", requireOnlyCommandInMessage = true) { 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 ratings = ratingsRepo.postsByRatings().toList().sortedByDescending { it.first }
val textSources = buildEntities { val textSources = buildEntities {
+ "Ratings amount: " + bold("${ratings.sumOf { it.second.size }}") + "\n\n" + "Ratings amount: " + bold("${ratings.sumOf { it.second.size }}") + "\n\n"
@@ -260,11 +267,11 @@ object Plugin : Plugin {
} }
} }
} }
includeRootNavigationButtonsHandler(setOf(chatConfig.sourceChatId), ratingsRepo, postsRepo) includeRootNavigationButtonsHandler(chatConfig.allSourceChatIds, ratingsRepo, postsRepo)
onMessageDataCallbackQuery("ratings_interactive", initialFilter = { it.message.chat.id == chatConfig.sourceChatId }) { onMessageDataCallbackQuery("ratings_interactive", initialFilter = { it.message.chat.id in chatConfig.allSourceChatIds }) {
edit( edit(
it.message, it.message,
ratingsRepo.buildRootButtons() replyMarkup = ratingsRepo.buildRootButtons()
) )
} }

View File

@@ -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.initTable
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import dev.inmo.plaguposter.common.ShortMessageInfo import dev.inmo.plaguposter.common.ShortMessageInfo
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.PollIdentifier
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull import org.jetbrains.exposed.sql.SqlExpressionBuilder.isNull
@@ -13,7 +11,7 @@ import org.jetbrains.exposed.sql.statements.*
class ExposedPollsToMessagesInfoRepo( class ExposedPollsToMessagesInfoRepo(
database: Database database: Database
) : PollsToMessagesInfoRepo, AbstractExposedKeyValueRepo<PollIdentifier, ShortMessageInfo>( ) : PollsToMessagesInfoRepo, AbstractExposedKeyValueRepo<PollId, ShortMessageInfo>(
database, database,
"polls_to_their_messages_info" "polls_to_their_messages_info"
) { ) {
@@ -21,31 +19,32 @@ class ExposedPollsToMessagesInfoRepo(
private val chatIdColumn = long("chat_id") private val chatIdColumn = long("chat_id")
private val threadIdColumn = long("thread_id").nullable().default(null) private val threadIdColumn = long("thread_id").nullable().default(null)
private val messageIdColumn = long("message_id") 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> = { override val selectByValue: ISqlExpressionBuilder.(ShortMessageInfo) -> Op<Boolean> = {
chatIdColumn.eq(it.chatId.chatId).and(it.chatId.threadId ?.let { threadIdColumn.eq(it) } ?: threadIdColumn.isNull()).and( chatIdColumn.eq(it.chatId.chatId.long)
messageIdColumn.eq(it.messageId) .and(it.chatId.threadId?.let { threadIdColumn.eq(it.long) } ?: threadIdColumn.isNull()).and(
messageIdColumn.eq(it.messageId.long)
) )
} }
override val ResultRow.asKey: PollIdentifier override val ResultRow.asKey: PollId
get() = get(keyColumn) get() = PollId(get(keyColumn))
override val ResultRow.asObject: ShortMessageInfo override val ResultRow.asObject: ShortMessageInfo
get() = ShortMessageInfo( get() = ShortMessageInfo(
IdChatIdentifier(get(chatIdColumn), get(threadIdColumn)), IdChatIdentifier(RawChatId(get(chatIdColumn)), get(threadIdColumn) ?.let(::MessageThreadId)),
get(messageIdColumn) MessageId(get(messageIdColumn))
) )
init { init {
initTable() initTable()
} }
override fun update(k: PollIdentifier, v: ShortMessageInfo, it: UpdateBuilder<Int>) { override fun update(k: PollId, v: ShortMessageInfo, it: UpdateBuilder<Int>) {
it[chatIdColumn] = v.chatId.chatId it[chatIdColumn] = v.chatId.chatId.long
it[threadIdColumn] = v.chatId.threadId it[threadIdColumn] = v.chatId.threadId ?.long
it[messageIdColumn] = v.messageId it[messageIdColumn] = v.messageId.long
} }
override fun insertKey(k: PollIdentifier, v: ShortMessageInfo, it: InsertStatement<Number>) { override fun insertKey(k: PollId, v: ShortMessageInfo, it: UpdateBuilder<Int>) {
it[keyColumn] = k it[keyColumn] = k.string
} }
} }

View File

@@ -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.initTable
import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo import dev.inmo.micro_utils.repos.exposed.keyvalue.AbstractExposedKeyValueRepo
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.types.PollIdentifier import dev.inmo.tgbotapi.types.PollId
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.* import org.jetbrains.exposed.sql.statements.*
class ExposedPollsToPostsIdsRepo( class ExposedPollsToPostsIdsRepo(
database: Database database: Database
) : PollsToPostsIdsRepo, AbstractExposedKeyValueRepo<PollIdentifier, PostId>(database, "polls_to_posts") { ) : PollsToPostsIdsRepo, AbstractExposedKeyValueRepo<PollId, PostId>(database, "polls_to_posts") {
override val keyColumn = text("poll_id") override val keyColumn = text("poll_id")
val postIdColumn = text("postId") 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 selectByValue: ISqlExpressionBuilder.(PostId) -> Op<Boolean> = { postIdColumn.eq(it.string) }
override val ResultRow.asKey: PollIdentifier override val ResultRow.asKey: PollId
get() = get(keyColumn) get() = PollId(get(keyColumn))
override val ResultRow.asObject: PostId override val ResultRow.asObject: PostId
get() = get(postIdColumn).let(::PostId) get() = get(postIdColumn).let(::PostId)
@@ -23,11 +23,11 @@ class ExposedPollsToPostsIdsRepo(
initTable() 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 it[postIdColumn] = v.string
} }
override fun insertKey(k: PollIdentifier, v: PostId, it: InsertStatement<Number>) { override fun insertKey(k: PollId, v: PostId, it: UpdateBuilder<Int>) {
it[keyColumn] = k it[keyColumn] = k.string
} }
} }

View File

@@ -0,0 +1,18 @@
package dev.inmo.plaguposter.ratings.models
import dev.inmo.krontab.EveryHourScheduler
import dev.inmo.krontab.KrontabTemplate
import dev.inmo.krontab.buildSchedule
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
@Serializable
internal data class RatingsConfig(
@SerialName("manualRecheckKrontab")
val manualRecheckKrontabTemplate: KrontabTemplate = "0 /30 *"
) {
@Transient
val manualRecheckKrontab
get() = buildSchedule(manualRecheckKrontabTemplate)
}

View File

@@ -1,8 +1,10 @@
package dev.inmo.plaguposter.ratings.repo package dev.inmo.plaguposter.ratings.repo
import dev.inmo.micro_utils.pagination.utils.doForAllWithNextPaging 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.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.micro_utils.repos.cache.full.FullKeyValueCacheRepo
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.models.Rating import dev.inmo.plaguposter.ratings.models.Rating
@@ -11,28 +13,32 @@ import kotlinx.coroutines.CoroutineScope
class CachedRatingsRepo( class CachedRatingsRepo(
private val base: RatingsRepo, private val base: RatingsRepo,
private val scope: CoroutineScope, 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) { ) : 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( override suspend fun getPosts(
range: ClosedRange<Rating>, range: ClosedRange<Rating>,
reversed: Boolean, reversed: Boolean,
count: Int?, count: Int?,
exclude: List<PostId> exclude: List<PostId>
): Map<PostId, Rating> { ): Map<PostId, Rating> = getPosts(reversed, count, exclude) {
val result = mutableMapOf<PostId, Rating>() it in range
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()
} }
override suspend fun getPostsWithRatingGreaterEq( override suspend fun getPostsWithRatingGreaterEq(
@@ -40,22 +46,16 @@ class CachedRatingsRepo(
reversed: Boolean, reversed: Boolean,
count: Int?, count: Int?,
exclude: List<PostId> exclude: List<PostId>
): Map<PostId, Rating> = getPosts( ): Map<PostId, Rating> = getPosts(reversed, count, exclude) {
then .. Rating(Double.MAX_VALUE), it >= then
reversed, }
count,
exclude
)
override suspend fun getPostsWithRatingLessEq( override suspend fun getPostsWithRatingLessEq(
then: Rating, then: Rating,
reversed: Boolean, reversed: Boolean,
count: Int?, count: Int?,
exclude: List<PostId> exclude: List<PostId>
): Map<PostId, Rating> = getPosts( ): Map<PostId, Rating> = getPosts(reversed, count, exclude) {
Rating(Double.MIN_VALUE) .. then, it <= then
reversed, }
count,
exclude
)
} }

View File

@@ -1,23 +1,30 @@
package dev.inmo.plaguposter.ratings package dev.inmo.plaguposter.ratings
import dev.inmo.krontab.utils.asTzFlowWithDelays
import dev.inmo.kslog.common.TagLogger
import dev.inmo.kslog.common.i
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithBinds import dev.inmo.micro_utils.koin.singleWithBinds
import dev.inmo.micro_utils.repos.unset import dev.inmo.micro_utils.repos.unset
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.config
import dev.inmo.plagubot.database
import dev.inmo.plagubot.registerConfig
import dev.inmo.plaguposter.common.useCache import dev.inmo.plaguposter.common.useCache
import dev.inmo.plaguposter.posts.exposed.ExposedPostsRepo
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.Plugin.setupBotPlugin
import dev.inmo.plaguposter.ratings.exposed.ExposedRatingsRepo import dev.inmo.plaguposter.ratings.exposed.ExposedRatingsRepo
import dev.inmo.plaguposter.ratings.models.RatingsConfig
import dev.inmo.plaguposter.ratings.repo.* import dev.inmo.plaguposter.ratings.repo.*
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.dsl.binds
object Plugin : Plugin { object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) { private val Log = TagLogger("RatingsPlugin")
override fun Module.setupDI(config: JsonObject) {
single { ExposedRatingsRepo(database) } single { ExposedRatingsRepo(database) }
singleWithBinds<RatingsRepo> { singleWithBinds<RatingsRepo> {
val base = get<ExposedRatingsRepo>() val base = get<ExposedRatingsRepo>()
@@ -28,12 +35,24 @@ object Plugin : Plugin {
base base
} }
} }
registerConfig(RatingsConfig.serializer(), "ratings") { RatingsConfig() }
} }
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { override suspend fun startPlugin(koin: Koin) {
super.startPlugin(koin)
val config = koin.config<RatingsConfig>()
val scope = koin.get<CoroutineScope>()
val ratingsRepo = koin.get<RatingsRepo>() val ratingsRepo = koin.get<RatingsRepo>()
koin.get<PostsRepo>().deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(this) { val postsRepo = koin.get<PostsRepo>()
postsRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(scope) {
ratingsRepo.unset(it) ratingsRepo.unset(it)
} }
config.manualRecheckKrontab.asTzFlowWithDelays().subscribeSafelyWithoutExceptions(scope) {
Log.i { "Start clearing ratings without registered posts" }
val postsIdsToRemove = ratingsRepo.getAll().keys - postsRepo.getAll().keys
Log.i { "Posts to remove: $postsIdsToRemove" }
ratingsRepo.unset(postsIdsToRemove.toList())
}
} }
} }

View File

@@ -33,7 +33,7 @@ class ExposedRatingsRepo (
it[ratingsColumn] = v.double it[ratingsColumn] = v.double
} }
override fun insertKey(k: PostId, v: Rating, it: InsertStatement<Number>) { override fun insertKey(k: PostId, v: Rating, it: UpdateBuilder<Int>) {
it[keyColumn] = k.string it[keyColumn] = k.string
} }
@@ -49,7 +49,7 @@ class ExposedRatingsRepo (
count: Int?, count: Int?,
exclude: List<PostId> exclude: List<PostId>
): Map<PostId, Rating> = transaction(database) { ): Map<PostId, Rating> = transaction(database) {
select { selectAll().where {
ratingsColumn.greaterEq(range.start.double).and( ratingsColumn.greaterEq(range.start.double).and(
ratingsColumn.lessEq(range.endInclusive.double) ratingsColumn.lessEq(range.endInclusive.double)
).and( ).and(
@@ -66,7 +66,7 @@ class ExposedRatingsRepo (
count: Int?, count: Int?,
exclude: List<PostId> exclude: List<PostId>
) = transaction(database) { ) = 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 it.asKey to it.asObject
} }
}.toMap() }.toMap()
@@ -77,7 +77,7 @@ class ExposedRatingsRepo (
count: Int?, count: Int?,
exclude: List<PostId> exclude: List<PostId>
): Map<PostId, Rating> = transaction(database) { ): 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 it.asKey to it.asObject
} }
}.toMap() }.toMap()

View File

@@ -22,6 +22,7 @@ dependencies {
api project(":plaguposter.ratings.source") api project(":plaguposter.ratings.source")
api project(":plaguposter.ratings.selector") api project(":plaguposter.ratings.selector")
api project(":plaguposter.ratings.gc") api project(":plaguposter.ratings.gc")
api project(":plaguposter.posts.gc")
api project(":plaguposter.inlines") api project(":plaguposter.inlines")
api libs.psql api libs.psql
@@ -32,6 +33,6 @@ application {
} }
java { java {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_17
} }

View File

@@ -1,85 +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.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.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"
},
"gc": {
"autoclear": {
"rating": -1,
"autoClearKrontab": "0 0 0 * *",
"skipPostAge": 86400
},
"immediateDrop": -6
}
}

View File

@@ -15,7 +15,7 @@ function assert_success() {
app=plaguposter app=plaguposter
version="`grep ../gradle.properties -e "^version=" | sed -e "s/version=\(.*\)/\1/"`" version="`grep ../gradle.properties -e "^version=" | sed -e "s/version=\(.*\)/\1/"`"
server=insanusmokrassar server=docker.inmo.dev
assert_success ../gradlew build assert_success ../gradlew build
assert_success sudo docker build -t $app:"$version" . assert_success sudo docker build -t $app:"$version" .

View File

@@ -1,5 +0,0 @@
DATA_PATH=.
PG_USER=test_user
PG_PASSWORD=test_password
PG_DB=test_db

View File

@@ -1,131 +1,85 @@
{ {
"database": { "database": {
"url": "jdbc:postgresql://postgres/test_db", "url": "jdbc:postgresql://postgres:5432/test",
"username": "test_user", "username": "test",
"password": "test_password", "password": "test",
"driver": "org.postgresql.Driver" "driver": "org.postgresql.Driver"
}, },
"botToken": "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi", "botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
"plugins": [ "plugins": [
"dev.inmo.plaguposter.posts.Plugin", "dev.inmo.plaguposter.posts.Plugin",
"dev.inmo.plaguposter.posts.registrar.Plugin", "dev.inmo.plaguposter.posts.registrar.Plugin",
"dev.inmo.plaguposter.ratings.Plugin", "dev.inmo.plaguposter.ratings.Plugin",
"dev.inmo.plaguposter.ratings.source.Plugin", "dev.inmo.plaguposter.ratings.source.Plugin",
"dev.inmo.plaguposter.ratings.selector.Plugin", "dev.inmo.plaguposter.ratings.selector.Plugin",
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plaguposter.ratings.gc.Plugin", "dev.inmo.plaguposter.ratings.gc.Plugin",
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plagubot.plugins.inline.queries.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": { "posts": {
"chats": { "chats": {
"targetChat": -1001234567890, "targetChat": 12345678,
"cacheChat": -1001234567890, "cacheChat": 12345678,
"sourceChat": -1001234567890 "sourceChat": 12345678,
}, "targetChats": [12345678],
"autoRemoveMessages": true "_note": "You must set targetChat or targetChats with at least one object"
}
}, },
"ratingsPolls": { "ratingsPolls": {
"variants": { "variants": {
"Круть": 2, "Cool": 2,
"Ок": 1, "Ok": 1,
"Не ок": -1, "Not ok": -1,
"Совсем не ок": -2, "Inappropriate": -2,
"Посмотреть результаты": 0 "Results": 0
}, },
"autoAttach": true, "autoAttach": true,
"ratingOfferText": "How do you like it?" "ratingOfferText": "What do you think about it?"
}, },
"selector": { "selector": {
"items": [ "items": [
{ {
"time": { "time": {
"from": "23:00", "from": "00:00",
"to": "23:59" "to": "23:59"
}, },
"rating": { "rating": {
"min": -1.0,
"max": 2.0,
"prefer": "max", "prefer": "max",
"otherwise": { "uniqueCount": 1
"rating": {
"min": 2.0,
"prefer": "min",
"postAge": 86400
}
},
"postAge": 86400
} }
}, },
{ {
"time": { "time": {
"from": "00:00", "from": "23:59",
"to": "06:59" "to": "00:00"
},
"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"
}, },
"rating": { "rating": {
"prefer": "max", "prefer": "max",
"postAge": 86400 "uniqueCount": 1
} }
} }
] ]
}, },
"timer_trigger": { "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": { "gc": {
"autoclear": { "autoclear": {
@@ -133,6 +87,11 @@
"autoClearKrontab": "0 0 0 * *", "autoClearKrontab": "0 0 0 * *",
"skipPostAge": 86400 "skipPostAge": 86400
}, },
"immediateDrop": -2 "immediateDrop": -6
},
"messagesChecker": {
"krontab": "0 0 0 * *",
"throttlingMillis": 1000,
"doFullCheck": false
} }
} }

View File

@@ -2,22 +2,24 @@ version: "3.4"
services: services:
plaguposter_postgres: plaguposter_postgres:
image: postgres image: postgres:15.4-bullseye
container_name: "plaguposter_postgres" container_name: "plaguposter_postgres"
restart: "unless-stopped" restart: "unless-stopped"
environment: environment:
POSTGRES_USER: "${PG_USER}" POSTGRES_USER: "test"
POSTGRES_PASSWORD: "${PG_PASSWORD}" POSTGRES_PASSWORD: "test"
POSTGRES_DB: "${PG_DB}" POSTGRES_DB: "test"
volumes: volumes:
- "${DATA_PATH}/db/:/var/lib/postgresql/" - "./db/:/var/lib/postgresql/data"
- "/etc/timezone:/etc/timezone:ro"
plaguposter: plaguposter:
image: insanusmokrassar/plaguposter image: insanusmokrassar/plaguposter:latest
container_name: "plaguposter" container_name: "plaguposter"
restart: "unless-stopped" restart: "unless-stopped"
volumes: volumes:
- "${DATA_PATH}/config.json:/config.json" - "./config.json:/config.json"
links: - "/etc/timezone:/etc/timezone:ro"
- "plaguposter_postgres:postgres"
depends_on: depends_on:
- "plaguposter_postgres" - "plaguposter_postgres"
links:
- "plaguposter_postgres:postgres"

View File

@@ -4,6 +4,7 @@ String[] includes = [
":common", ":common",
":posts", ":posts",
":posts:panel", ":posts:panel",
":posts:gc",
":posts_registrar", ":posts_registrar",
":ratings", ":ratings",
":ratings:source", ":ratings:source",
@@ -29,5 +30,3 @@ includes.each { originalName ->
project.name = projectName project.name = projectName
project.projectDir = new File(projectDirectory) project.projectDir = new File(projectDirectory)
} }
enableFeaturePreview("VERSION_CATALOGS")

View File

@@ -7,6 +7,7 @@ import dev.inmo.plaguposter.common.SuccessfulSymbol
import dev.inmo.plagubot.plugins.inline.queries.models.Format 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.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plagubot.registerConfig
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder
import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI
@@ -24,7 +25,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.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.types.ChatId 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.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.message.textsources.regular import dev.inmo.tgbotapi.types.message.textsources.regular
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -38,17 +39,15 @@ object Plugin : Plugin {
@Serializable @Serializable
private data class PublishState( private data class PublishState(
override val context: ChatId, override val context: ChatId,
val sourceMessageId: MessageIdentifier, val sourceMessageId: MessageId,
val messageInReply: MessageIdentifier val messageInReply: MessageId
) : State ) : State
@Serializable @Serializable
internal data class Config( internal data class Config(
val panelButtonText: String? = "Publish" val panelButtonText: String? = "Publish"
) )
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(params: JsonObject) {
params["publish_command"] ?.let { configJson -> registerConfig<Config>("publish_command") { null }
single { get<Json>().decodeFromJsonElement(Config.serializer(), configJson) }
}
} }
override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) {
@@ -77,7 +76,7 @@ object Plugin : Plugin {
return@onCommand return@onCommand
} }
} ?: selector ?.take(1) ?.firstOrNull() } ?: selector ?.takeOneOrNull()
if (postId == null) { if (postId == null) {
reply( reply(
it, it,

View File

@@ -16,6 +16,7 @@ import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.plugins.inline.queries.models.Format 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.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plagubot.registerConfig
import dev.inmo.plaguposter.common.ChatConfig import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.ReadPostsRepo import dev.inmo.plaguposter.posts.repo.ReadPostsRepo
@@ -51,7 +52,8 @@ object Plugin : Plugin {
internal data class Config( internal data class Config(
@SerialName("krontab") @SerialName("krontab")
val krontabTemplate: KrontabTemplate, val krontabTemplate: KrontabTemplate,
val dateTimeFormat: String = "HH:mm:ss, dd.MM.yyyy" val dateTimeFormat: String = "HH:mm:ss, dd.MM.yyyy",
val retryOnPostFailureTimes: Int = 0
) { ) {
@Transient @Transient
val krontab by lazy { val krontab by lazy {
@@ -61,8 +63,8 @@ object Plugin : Plugin {
@Transient @Transient
val format: DateFormat = DateFormat(dateTimeFormat) val format: DateFormat = DateFormat(dateTimeFormat)
} }
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(params: JsonObject) {
single { get<Json>().decodeFromJsonElement(Config.serializer(), params["timer_trigger"] ?: return@single null) } registerConfig<Config>("timer_trigger") { null }
} }
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
@@ -88,13 +90,27 @@ object Plugin : Plugin {
} }
val krontab = koin.get<Config>().krontab val krontab = koin.get<Config>().krontab
val retryOnPostFailureTimes = koin.get<Config>().retryOnPostFailureTimes
val dateTimeFormat = koin.get<Config>().format val dateTimeFormat = koin.get<Config>().format
krontab.asFlowWithDelays().subscribeSafelyWithoutExceptions(this) { dateTime -> krontab.asFlowWithDelays().subscribeSafelyWithoutExceptions(this) { dateTime ->
selector.take(now = dateTime).forEach { postId -> var leftRetries = retryOnPostFailureTimes
do {
val success = runCatching {
selector.takeOneOrNull(now = dateTime) ?.let { postId ->
if (filters.all { it.check(postId, dateTime) }) { if (filters.all { it.check(postId, dateTime) }) {
publisher.publish(postId) publisher.publish(postId)
} else {
false
} }
} ?: false
}.getOrElse {
false
} }
if (success) {
break;
}
leftRetries--;
} while (leftRetries > 0)
} }
suspend fun buildPage(pagination: Pagination = FirstPagePagination(size = pageCallbackDataQuerySize)): InlineKeyboardMarkup { suspend fun buildPage(pagination: Pagination = FirstPagePagination(size = pageCallbackDataQuerySize)): InlineKeyboardMarkup {
@@ -113,7 +129,7 @@ object Plugin : Plugin {
val selected = mutableListOf<PostId>() val selected = mutableListOf<PostId>()
krontab.asFlowWithoutDelays().take(pagination.lastIndexExclusive).collectIndexed { i, dateTime -> krontab.asFlowWithoutDelays().take(pagination.lastIndexExclusive).collectIndexed { i, dateTime ->
val postId = selector.take(now = dateTime, exclude = selected).firstOrNull() ?.also { postId -> val postId = selector.takeOneOrNull(now = dateTime, exclude = selected) ?.also { postId ->
if (filters.all { it.check(postId, dateTime) }) { if (filters.all { it.check(postId, dateTime) }) {
selected.add(postId) selected.add(postId)
} else { } else {
@@ -136,7 +152,7 @@ object Plugin : Plugin {
} }
} }
onCommand("autoschedule_panel", initialFilter = { it.sameChat(chatConfig.sourceChatId) }) { onCommand("autoschedule_panel", initialFilter = { chatConfig.allSourceChatIds.any { chatId -> it.sameChat(chatId) } }) {
val keyboard = buildPage() val keyboard = buildPage()
runCatchingSafely { runCatchingSafely {
@@ -152,7 +168,7 @@ object Plugin : Plugin {
onMessageDataCallbackQuery( onMessageDataCallbackQuery(
Regex("^$pageCallbackDataQueryPrefix\\d+"), Regex("^$pageCallbackDataQueryPrefix\\d+"),
initialFilter = { it.message.sameChat(chatConfig.sourceChatId) } initialFilter = { chatConfig.allSourceChatIds.any { sourceChatId -> it.message.sameChat(sourceChatId) } }
) { ) {
val page = it.data.removePrefix(pageCallbackDataQueryPrefix).toIntOrNull() ?: let { _ -> val page = it.data.removePrefix(pageCallbackDataQueryPrefix).toIntOrNull() ?: let { _ ->
answer(it) answer(it)

View File

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

View File

@@ -8,11 +8,10 @@ import dev.inmo.plaguposter.ratings.repo.RatingsRepo
import dev.inmo.plaguposter.triggers.timer.TimersRepo import dev.inmo.plaguposter.triggers.timer.TimersRepo
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module import org.koin.core.module.Module
object Plugin : Plugin { object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(config: JsonObject) {
singleWithRandomQualifier(createdAtStart = true) { singleWithRandomQualifier(createdAtStart = true) {
val timersRepo = get<TimersRepo>() val timersRepo = get<TimersRepo>()
val ratingsRepo = get<RatingsRepo>() val ratingsRepo = get<RatingsRepo>()

View File

@@ -7,6 +7,7 @@ import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.unset import dev.inmo.micro_utils.repos.unset
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.sending.PostPublisher import dev.inmo.plaguposter.posts.sending.PostPublisher
import korlibs.time.millisecondsLong
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay

View File

@@ -1,7 +1,5 @@
package dev.inmo.plaguposter.triggers.timer package dev.inmo.plaguposter.triggers.timer
import korlibs.time.DateTime
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithRandomQualifierAndBinds import dev.inmo.micro_utils.koin.singleWithRandomQualifierAndBinds
import dev.inmo.micro_utils.repos.set import dev.inmo.micro_utils.repos.set
@@ -9,24 +7,19 @@ import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.common.ChatConfig import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI
import dev.inmo.plaguposter.posts.repo.ReadPostsRepo
import dev.inmo.plaguposter.triggers.timer.repo.ExposedTimersRepo import dev.inmo.plaguposter.triggers.timer.repo.ExposedTimersRepo
import dev.inmo.tgbotapi.extensions.api.answers.answer import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.reply
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext 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.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.dsl.binds import org.koin.dsl.binds
object Plugin : Plugin { object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(config: JsonObject) {
single { ExposedTimersRepo(get(), get(), get()) } binds arrayOf(TimersRepo::class) single { ExposedTimersRepo(get(), get(), get()) } binds arrayOf(TimersRepo::class)
single(createdAtStart = true) { TimersHandler(get(), get(), get()) } single(createdAtStart = true) { TimersHandler(get(), get(), get()) }
singleWithRandomQualifierAndBinds { TimerPanelButton(get()) } singleWithRandomQualifierAndBinds { TimerPanelButton(get()) }

View File

@@ -50,7 +50,7 @@ class ExposedTimersRepo(
it[dateTimeColumn] = v.unixMillisLong it[dateTimeColumn] = v.unixMillisLong
} }
override fun insertKey(k: PostId, v: DateTime, it: InsertStatement<Number>) { override fun insertKey(k: PostId, v: DateTime, it: UpdateBuilder<Int>) {
it[keyColumn] = k.string it[keyColumn] = k.string
} }