69 Commits

Author SHA1 Message Date
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
54eb7515d3 small fixes in repos 2023-08-13 00:02:19 +06:00
1bb12bee0e fix of build 2023-08-12 23:58:28 +06:00
467525e48d fill changelog 2023-08-12 23:52:53 +06:00
29e5a04135 update dependencies && add opportunity to use several targetChatIds instead of one 2023-08-12 23:50:56 +06:00
6eb43055a7 0.2.3 2023-08-12 23:31:30 +06:00
57eebb61d5 0.2.2 2023-05-10 11:05:14 +06:00
87957dba30 update dependenies. 0.2.1 2023-05-07 19:05:30 +06:00
20148c02f0 0.2.0 2023-04-25 00:48:39 +06:00
e17cfa1c7c update dependencies 2023-04-20 00:41:19 +06:00
0a5ffee808 improvements 2023-04-06 15:52:44 +06:00
847b285ce3 Merge pull request #15 from InsanusMokrassar/0.1.1
0.1.1
2023-03-30 12:14:07 +06:00
c449457d86 update krontab 2023-03-18 14:11:40 +06:00
1b3a632d7b replace OfferTemplate 2023-03-18 14:05:09 +06:00
ebfa79cf64 add OfferTemplate and change autoschedule command 2023-03-18 13:59:37 +06:00
e59c7b0f7e fixes 2023-03-18 13:48:26 +06:00
7a4fb05bfb add publishing_autoschedule 2023-03-18 13:12:05 +06:00
7bc7bf6e8c update dependencies 2023-03-18 12:35:18 +06:00
c64faf75d0 start 0.1.1 2023-03-18 12:28:35 +06:00
ee80d8a3a1 Merge pull request #14 from InsanusMokrassar/0.1.0
0.1.0
2023-03-14 23:58:49 +06:00
22c94a4c43 update base image 2023-03-12 22:56:20 +06:00
c6bcfc0068 upgrade version retieving from gradle.properties 2023-03-12 22:49:34 +06:00
8a648cb066 fixes in docker publishing script 2023-03-12 22:41:41 +06:00
345a156334 fixes 2023-03-12 22:38:21 +06:00
7fb7f923f7 Update libs.versions.toml 2023-03-12 17:37:52 +06:00
3b858a3c00 Update libs.versions.toml 2023-03-09 12:00:57 +06:00
f09e80b8bd fixes for building 2023-03-05 23:30:22 +06:00
fea25743d5 Update libs.versions.toml 2023-03-05 00:55:00 +06:00
86183f5f74 Update libs.versions.toml 2023-02-28 14:25:45 +06:00
bc8d0b26bd start 0.1.0 2023-02-28 14:23:35 +06:00
b05844737b Merge pull request #13 from InsanusMokrassar/0.0.10
0.0.10
2023-02-21 22:13:19 +06:00
67 changed files with 889 additions and 477 deletions

View File

@@ -8,10 +8,10 @@ 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: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"

View File

@@ -1,8 +1,7 @@
name: Docker name: Docker
on:
push: on: [push]
branches:
- master
jobs: jobs:
publishing: publishing:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -12,6 +11,17 @@ 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
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
if [[ "$branch" != "master" ]]; then
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
rm gradle.properties
mv gradle.properties.tmp gradle.properties
fi
- name: Log into registry - name: Log into registry
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with: with:

View File

@@ -1,8 +1,86 @@
# PlaguPoster # PlaguPoster
## 0.5.5
* Dependencies update
## 0.5.4
* Dependencies update
## 0.5.3
* Dependencies update
## 0.5.2
* Dependencies update
## 0.5.1
* Add opportunity to set unique
## 0.5.0
* Dependencies update
* Since this update bots will require **`JDK` 17+**
## 0.3.0
* `Versions`:
* `tgbotapi`: `9.1.0`
* `plagubot`: `7.1.0`
* `plagubot-plugins`: `0.14.0`
## 0.2.3
* Add opportunity to use several target chat ids
* Update dependencies
## 0.2.2
* `GarbageCollector`:
* Now on start will all clearing job done
## 0.2.1
* `Versions`:
* `kotlin`: `1.8.21`
* `tgbotapi`: `7.1.2`
* `plagubot`: `5.1.2`
* `microutils`: `0.18.1`
* `kslog`: `1.1.1`
* `plagubot.plugins`: `0.11.2`
* `psql`: `42.6.0`
## 0.2.0
* `Versions`:
* `tgbotapi`: `7.1.0`
* `plagubot`: `5.1.0`
* `krontab`: `1.0.0`
* `plagubot.plugins`: `0.11.0`
## 0.1.2
* `Versions`:
* `kotlin`: `1.8.20`
* `plagubot`: `5.0.2`
* `microutils`: `0.17.8`
* `kslog`: `1.1.1`
* `plagubot.plugins`: `0.10.2`
* `psql`: `42.6.0`
## 0.1.1
* Update dependencies
* `Triggers`
* `SelectorWithTimer`
* Opportunity to get schedule of posts using `publishing_autoschedule` command
## 0.0.10 ## 0.0.10
## 0.0.9 ## 0.0.9
* Update depedencies * Update dependencies

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

@@ -1,6 +1,5 @@
package dev.inmo.plaguposter.common package dev.inmo.plaguposter.common
import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer import dev.inmo.tgbotapi.types.FullChatIdentifierSerializer
import dev.inmo.tgbotapi.types.IdChatIdentifier import dev.inmo.tgbotapi.types.IdChatIdentifier
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
@@ -10,17 +9,34 @@ import kotlinx.serialization.Serializable
data class ChatConfig( data class ChatConfig(
@SerialName("targetChat") @SerialName("targetChat")
@Serializable(FullChatIdentifierSerializer::class) @Serializable(FullChatIdentifierSerializer::class)
val targetChatId: IdChatIdentifier, 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")
val targetChatIds: List<@Serializable(FullChatIdentifierSerializer::class) IdChatIdentifier> = emptyList(),
@SerialName("sourceChats")
val sourceChatIds: List<@Serializable(FullChatIdentifierSerializer::class) IdChatIdentifier> = emptyList(),
) { ) {
val allTargetChatIds by lazy {
(listOfNotNull(targetChatId) + targetChatIds).toSet()
}
val allSourceChatIds by lazy {
(listOfNotNull(sourceChatId) + sourceChatIds).toSet()
}
init {
require(targetChatId != null || targetChatIds.isNotEmpty()) {
"One of fields, 'targetChat' or 'targetChats' should be presented"
}
}
fun check(chatId: IdChatIdentifier) = when (chatId) { fun check(chatId: IdChatIdentifier) = when (chatId) {
targetChatId, in allTargetChatIds,
sourceChatId, in allSourceChatIds,
cacheChatId -> true cacheChatId -> true
else -> false else -> false
} }

View File

@@ -2,13 +2,14 @@ package dev.inmo.plaguposter.common
import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.CommonMessageFilterExcludeMediaGroups import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.CommonMessageFilterExcludeMediaGroups
import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.SimpleFilter import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.SimpleFilter
import dev.inmo.tgbotapi.extensions.utils.contentMessageOrNull
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
import dev.inmo.tgbotapi.extensions.utils.withContentOrNull
import dev.inmo.tgbotapi.types.BotCommand import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.message.abstracts.* import dev.inmo.tgbotapi.types.message.abstracts.*
import dev.inmo.tgbotapi.types.message.content.TextContent
import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource
val FirstSourceIsCommandsFilter = SimpleFilter<Message> { val FirstSourceIsCommandsFilter = SimpleFilter<Message> {
it is ContentMessage<*> && it.content.textContentOrNull() ?.textSources ?.firstOrNull { it.contentMessageOrNull() ?.withContentOrNull<TextContent>() ?.content ?.textSources ?.firstOrNull() is BotCommandTextSource
it is BotCommandTextSource
} != null
} }

View File

@@ -1,6 +1,6 @@
package dev.inmo.plaguposter.common package dev.inmo.plaguposter.common
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer import kotlinx.serialization.Serializer
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
@@ -8,7 +8,6 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
@Serializer(DateTime::class)
object DateTimeSerializer : KSerializer<DateTime> { object DateTimeSerializer : KSerializer<DateTime> {
override val descriptor: SerialDescriptor = Double.serializer().descriptor override val descriptor: SerialDescriptor = Double.serializer().descriptor
override fun deserialize(decoder: Decoder): DateTime = DateTime(decoder.decodeDouble()) override fun deserialize(decoder: Decoder): DateTime = DateTime(decoder.decodeDouble())

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

@@ -1,6 +1,5 @@
package dev.inmo.plaguposter.common package dev.inmo.plaguposter.common
import dev.inmo.kslog.common.i
import dev.inmo.kslog.common.iS import dev.inmo.kslog.common.iS
import dev.inmo.kslog.common.logger import dev.inmo.kslog.common.logger
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
@@ -27,8 +26,8 @@ object CommonPlugin : Plugin {
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val config = koin.get<ChatConfig>() val config = koin.get<ChatConfig>()
Log.iS { "Target chat info: ${getChat(config.targetChatId)}" } 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

@@ -10,4 +10,4 @@ android.enableJetifier=true
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.0.10 version=0.5.5

View File

@@ -1,19 +1,18 @@
[versions] [versions]
kotlin = "1.7.22" kotlin = "1.9.23"
kotlin-serialization = "1.4.1" kotlin-serialization = "1.6.3"
plagubot = "3.5.0" plagubot = "8.3.0"
tgbotapi = "5.2.1" tgbotapi = "12.0.1"
microutils = "0.16.11" microutils = "0.20.45"
kslog = "0.5.4" kslog = "1.3.3"
krontab = "0.8.5" krontab = "2.2.9"
tgbotapi-libraries = "0.8.2" plagubot-plugins = "0.18.3"
plagubot-plugins = "0.8.1"
dokka = "1.7.20" dokka = "1.9.20"
psql = "42.5.0" psql = "42.6.0"
[libraries] [libraries]

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-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

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

@@ -10,6 +10,14 @@ kotlin {
commonMain { commonMain {
dependencies { dependencies {
api project(":plaguposter.common") 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,194 @@
package dev.inmo.plaguposter.posts.gc
import com.benasher44.uuid.uuid4
import dev.inmo.krontab.KrontabTemplate
import dev.inmo.krontab.toKronScheduler
import dev.inmo.krontab.utils.asFlowWithDelays
import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.i
import dev.inmo.kslog.common.iS
import dev.inmo.micro_utils.coroutines.actor
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.repos.deleteById
import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.posts.models.NewPost
import dev.inmo.plaguposter.posts.models.PostContentInfo
import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.tgbotapi.extensions.api.delete
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.forwardMessage
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitInlineMessageIdDataCallbackQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.behaviour_builder.oneOf
import dev.inmo.tgbotapi.extensions.behaviour_builder.parallel
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.types.MilliSeconds
import dev.inmo.tgbotapi.utils.bold
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.Koin
import org.koin.core.module.Module
object Plugin : Plugin {
@Serializable
internal data class Config (
val krontab: KrontabTemplate? = null,
val throttlingMillis: MilliSeconds = 1000,
val doFullCheck: Boolean = false
)
override fun Module.setupDI(database: Database, params: JsonObject) {
params["messagesChecker"] ?.let { element ->
single { get<Json>().decodeFromJsonElement(Config.serializer(), element) }
}
}
private val gcLogger = KSLog("GarbageCollector")
private suspend fun BehaviourContext.doRecheck(
throttlingMillis: MilliSeconds,
doFullCheck: Boolean,
postsRepo: PostsRepo,
chatsConfig: ChatConfig
) {
val posts = postsRepo.getAll()
gcLogger.i {
"Start garbage collecting of posts. Initial posts count: ${posts.size}"
}
posts.forEach { (postId, post) ->
val surelyAbsentMessages = mutableListOf<PostContentInfo>()
for (content in post.content) {
try {
forwardMessage(
toChatId = chatsConfig.cacheChatId,
fromChatId = content.chatId,
messageId = content.messageId
)
if (!doFullCheck) {
break
}
} catch (e: Throwable) {
if (e.message ?.contains("message to forward not found") == true) {
surelyAbsentMessages.add(content)
}
}
delay(throttlingMillis)
}
val existsPostMessages = post.content.filter {
it !in surelyAbsentMessages
}
if (existsPostMessages.isNotEmpty() && surelyAbsentMessages.isNotEmpty()) {
runCatching {
postsRepo.update(
postId,
NewPost(
content = existsPostMessages
)
)
}
}
if (existsPostMessages.isNotEmpty()) {
return@forEach
}
runCatching {
send(
chatsConfig.cacheChatId,
"Can't find any messages for post $postId. So, deleting it"
)
}
runCatching {
postsRepo.deleteById(postId)
}
}
gcLogger.iS {
"Complete garbage collecting of posts. Result posts count: ${postsRepo.count()}"
}
}
override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) {
val postsRepo = koin.get<PostsRepo>()
val chatsConfig = koin.get<ChatConfig>()
val config = koin.getOrNull<Config>() ?: Config()
val scope = koin.get<CoroutineScope>()
val recheckActor = scope.actor<Unit>(0) {
runCatching {
doRecheck(
config.throttlingMillis,
config.doFullCheck,
postsRepo,
chatsConfig
)
}
}
config.krontab ?.toKronScheduler() ?.asFlowWithDelays() ?.subscribeSafelyWithoutExceptions(koin.get()) {
recheckActor.trySend(Unit)
}
onCommand("force_garbage_collection") { message ->
launch {
val prefix = uuid4().toString()
val yesData = "${prefix}yes"
val noData = "${prefix}no"
edit(
message,
text = "Are you sure want to trigger posts garbage collecting?",
replyMarkup = flatInlineKeyboard {
dataButton("Sure", yesData)
dataButton("No", noData)
}
)
val answer = oneOf(
parallel {
waitMessageDataCallbackQuery().filter {
it.message.sameMessage(message)
}.first()
},
parallel {
waitInlineMessageIdDataCallbackQuery().filter {
it.data == yesData || it.data == noData
}.first()
}
)
if (answer.data == yesData) {
if (recheckActor.trySend(Unit).isSuccess) {
edit(message, "Checking of posts without exists messages triggered")
} else {
edit(message) {
+"Checking of posts without exists messages has been triggered " + bold("earlier")
}
}
} else {
delete(message)
}
}
}
koin.getOrNull<InlineTemplatesRepo>() ?.addTemplate(
OfferTemplate(
"Force posts check",
listOf(
Format("/force_garbage_collection")
),
"Force check posts without exists messages"
)
)
}
}

View File

@@ -4,14 +4,11 @@ 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.plaguposter.common.ChatConfig import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.common.UnsuccessfulSymbol import dev.inmo.plaguposter.common.UnsuccessfulSymbol
@@ -33,7 +30,8 @@ import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton import dev.inmo.tgbotapi.extensions.utils.types.buttons.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
@@ -99,7 +97,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 +113,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 +129,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 +147,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 +156,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 +183,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,6 +1,6 @@
package dev.inmo.plaguposter.posts.models package dev.inmo.plaguposter.posts.models
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.plaguposter.common.DateTimeSerializer import dev.inmo.plaguposter.common.DateTimeSerializer
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.ChatId
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

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

@@ -1,14 +1,14 @@
package dev.inmo.plaguposter.posts.repo package dev.inmo.plaguposter.posts.repo
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.micro_utils.repos.ReadCRUDRepo import dev.inmo.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,20 +11,19 @@ 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(
private val bot: TelegramBot, private val bot: TelegramBot,
private val postsRepo: PostsRepo, private val postsRepo: PostsRepo,
private val cachingChatId: IdChatIdentifier, private val cachingChatId: IdChatIdentifier,
private val targetChatId: 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,20 +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 {
runCatching { targetChatIds.forEach { targetChatId ->
bot.copyMessage(targetChatId, it.chatId, it.messageId)
}.onFailure { _ ->
runCatching { runCatching {
bot.forwardMessage( bot.copyMessage(targetChatId, it.chatId, it.messageId)
it.chatId, }.onFailure { _ ->
targetChatId, runCatching {
it.messageId bot.forwardMessage(
) fromChatId = it.chatId,
toChatId = cachingChatId,
messageId = it.messageId
)
}.onSuccess {
bot.copyMessage(targetChatId, it)
haveSentMessages = true
}
}.onSuccess { }.onSuccess {
bot.copyMessage(targetChatId, it) haveSentMessages = true
} }
} }
return@forEach return@forEach
@@ -57,17 +62,26 @@ class PostPublisher(
it.order to (bot.forwardMessage(toChatId = cachingChatId, fromChatId = it.chatId, messageId = it.messageId).contentMessageOrNull() ?: return@mapNotNull null) it.order to (bot.forwardMessage(toChatId = cachingChatId, fromChatId = it.chatId, messageId = it.messageId).contentMessageOrNull() ?: return@mapNotNull null)
}.sortedBy { it.first }.mapNotNull { (_, forwardedMessage) -> }.sortedBy { it.first }.mapNotNull { (_, forwardedMessage) ->
forwardedMessage.withContentOrNull<MediaGroupPartContent>() ?: null.also { _ -> forwardedMessage.withContentOrNull<MediaGroupPartContent>() ?: null.also { _ ->
bot.copyMessage(targetChatId, forwardedMessage) targetChatIds.forEach { targetChatId ->
bot.copyMessage(targetChatId, forwardedMessage)
haveSentMessages = true
}
} }
} }
resultContents.singleOrNull() ?.also { resultContents.singleOrNull() ?.also {
bot.copyMessage(targetChatId, it) targetChatIds.forEach { targetChatId ->
bot.copyMessage(targetChatId, it)
haveSentMessages = true
}
return@forEach return@forEach
} ?: resultContents.chunked(mediaCountInMediaGroup.last).forEach { } ?: resultContents.chunked(mediaCountInMediaGroup.last).forEach {
bot.send( targetChatIds.forEach { targetChatId ->
targetChatId, bot.send(
it.map { it.content.toMediaGroupMemberTelegramMedia() } targetChatId,
) it.map { it.content.toMediaGroupMemberTelegramMedia() }
)
haveSentMessages = true
}
} }
} }
@@ -75,5 +89,6 @@ class PostPublisher(
postsRepo.deleteById(postId) postsRepo.deleteById(postId)
} }
return haveSentMessages
} }
} }

View File

@@ -29,7 +29,6 @@ import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database 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 {
@Serializable @Serializable
@@ -59,7 +58,7 @@ object Plugin : Plugin {
} }
single { single {
val config = get<Config>() val config = get<Config>()
PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.targetChatId, config.deleteAfterPublishing) PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.allTargetChatIds.toList(), config.deleteAfterPublishing)
} }
} }

View File

@@ -1,10 +1,13 @@
package dev.inmo.plaguposter.posts.cached package dev.inmo.plaguposter.posts.cached
import com.soywiz.klock.DateTime import dev.inmo.micro_utils.coroutines.SmartRWLocker
import korlibs.time.DateTime
import dev.inmo.micro_utils.pagination.FirstPagePagination import dev.inmo.micro_utils.pagination.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,23 +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,
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

@@ -1,7 +1,7 @@
package dev.inmo.plaguposter.posts.exposed package dev.inmo.plaguposter.posts.exposed
import com.benasher44.uuid.uuid4 import com.benasher44.uuid.uuid4
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.micro_utils.repos.KeyValuesRepo import dev.inmo.micro_utils.repos.KeyValuesRepo
import dev.inmo.micro_utils.repos.UpdatedValuePair import dev.inmo.micro_utils.repos.UpdatedValuePair
import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo import dev.inmo.micro_utils.repos.exposed.AbstractExposedCRUDRepo
@@ -10,7 +10,7 @@ import dev.inmo.plaguposter.posts.models.*
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.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(
@@ -81,7 +82,9 @@ class ExposedPostsRepo(
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
} }
} }
@@ -150,13 +153,13 @@ class ExposedPostsRepo(
} }
} }
override suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageIdentifier): PostId? { override suspend fun getIdByChatAndMessage(chatId: IdChatIdentifier, messageId: MessageId): PostId? {
return transaction(database) { return transaction(database) {
with(contentRepo) { with(contentRepo) {
select { select {
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)
} }

View File

@@ -18,85 +18,95 @@ import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextWithFSM
import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.* import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.*
import dev.inmo.tgbotapi.extensions.behaviour_builder.strictlyOn import dev.inmo.tgbotapi.extensions.behaviour_builder.strictlyOn
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.* import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.*
import dev.inmo.tgbotapi.extensions.utils.extensions.raw.text
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.formatting.buildEntities
import dev.inmo.tgbotapi.extensions.utils.textContentOrNull import dev.inmo.tgbotapi.extensions.utils.textContentOrNull
import dev.inmo.tgbotapi.extensions.utils.types.buttons.* import dev.inmo.tgbotapi.extensions.utils.types.buttons.*
import dev.inmo.tgbotapi.types.message.abstracts.ContentMessage
import dev.inmo.tgbotapi.types.message.content.MediaGroupContent
import dev.inmo.tgbotapi.types.message.content.MessageContent
import dev.inmo.tgbotapi.utils.regular import dev.inmo.tgbotapi.utils.regular
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.core.Koin import org.koin.core.Koin
@Serializable @Serializable
object Plugin : Plugin { object Plugin : Plugin {
@Serializable
data class Config(
val useInlineFinishingOpportunity: Boolean = true
)
override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) { override suspend fun BehaviourContextWithFSM<State>.setupBotPlugin(koin: Koin) {
val config = koin.get<ChatConfig>() val config = koin.get<ChatConfig>()
val postsRepo = koin.get<PostsRepo>() val postsRepo = koin.get<PostsRepo>()
strictlyOn {state: RegistrationState.InProcess -> strictlyOn { state: RegistrationState.InProcess ->
val buttonUuid = "finish" val buttonUuid = "finish"
val messageToDelete = send( val suggestionMessageDeferred = async {
state.context, send(
dev.inmo.tgbotapi.utils.buildEntities { state.context,
if (state.messages.isNotEmpty()) { dev.inmo.tgbotapi.utils.buildEntities {
regular("Your message(s) has been registered. You may send new ones or push \"Finish\" to finalize your post") if (state.messages.isNotEmpty()) {
regular("Your message(s) has been registered. You may send new ones or push \"Finish\" to finalize your post")
} else {
regular("Ok, send me your messages for new post")
}
},
replyMarkup = if (state.messages.isNotEmpty()) {
flatInlineKeyboard {
dataButton(
"Finish",
buttonUuid
)
}
} else { } else {
regular("Ok, send me your messages for new post") null
} }
}, )
replyMarkup = if (state.messages.isNotEmpty()) { }
flatInlineKeyboard {
dataButton(
"Finish",
buttonUuid
)
}
} else {
null
}
)
val newMessagesInfo = firstOf { firstOf {
add { add {
listOf( val receivedMessage = waitAnyContentMessage().filter {
waitAnyContentMessage().filter { it.sameChat(state.context)
it.chat.id == state.context && it.content.textContentOrNull() ?.text != "/finish_post" }.first()
}.take(1).first()
) when {
receivedMessage.content.textContentOrNull() ?.text == "/finish_post" -> {
val messageToDelete = suggestionMessageDeferred.await()
edit(messageToDelete, "Ok, finishing your request")
RegistrationState.Finish(
state.context,
state.messages
)
}
else -> {
RegistrationState.InProcess(
state.context,
state.messages + PostContentInfo.fromMessage(receivedMessage)
).also {
runCatchingSafely {
suggestionMessageDeferred.cancel()
}
runCatchingSafely {
delete(suggestionMessageDeferred.await())
}
}
}
}
} }
add { add {
val messageToDelete = suggestionMessageDeferred.await()
val finishPressed = waitMessageDataCallbackQuery().filter { val finishPressed = waitMessageDataCallbackQuery().filter {
it.message.sameMessage(messageToDelete) && it.data == buttonUuid it.message.sameMessage(messageToDelete) && it.data == buttonUuid
}.first() }.first()
emptyList<ContentMessage<MessageContent>>()
}
add {
val finishPressed = waitTextMessage().filter {
it.sameChat(messageToDelete) && it.content.text == "/finish_post"
}.first()
emptyList<ContentMessage<MessageContent>>()
}
}.ifEmpty {
edit(messageToDelete, "Ok, finishing your request")
return@strictlyOn RegistrationState.Finish(
state.context,
state.messages
)
}.flatMap {
PostContentInfo.fromMessage(it)
}
RegistrationState.InProcess( edit(messageToDelete, "Ok, finishing your request")
state.context, RegistrationState.Finish(
state.messages + newMessagesInfo state.context,
).also { state.messages
delete(messageToDelete) )
}
} }
} }
@@ -109,12 +119,12 @@ object Plugin : Plugin {
null null
} }
onCommand("start_post", initialFilter = { it.chat.id == config.sourceChatId }) { onCommand("start_post", initialFilter = { config.allSourceChatIds.any { chatId -> it.sameChat(chatId) } }) {
startChain(RegistrationState.InProcess(it.chat.id, emptyList())) startChain(RegistrationState.InProcess(it.chat.id, emptyList()))
} }
onContentMessage( onContentMessage(
initialFilter = { it.chat.id == config.sourceChatId && !FirstSourceIsCommandsFilter(it) } initialFilter = { config.allSourceChatIds.any { chatId -> it.sameChat(chatId) } && !FirstSourceIsCommandsFilter(it) }
) { ) {
startChain(RegistrationState.Finish(it.chat.id, PostContentInfo.fromMessage(it))) 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

@@ -1,18 +1,19 @@
package dev.inmo.plaguposter.ratings.gc package dev.inmo.plaguposter.ratings.gc
import com.soywiz.klock.milliseconds import korlibs.time.DateTime
import com.soywiz.klock.seconds 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.asFlow import dev.inmo.krontab.utils.asFlowWithDelays
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.repos.* import dev.inmo.micro_utils.repos.*
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
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.types.MilliSeconds
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.*
@@ -43,21 +44,35 @@ object Plugin : Plugin {
val config = koin.get<Config>() val config = koin.get<Config>()
config.immediateDrop ?.let { toDrop -> config.immediateDrop ?.let { toDrop ->
ratingsRepo.onNewValue.subscribeSafelyWithoutExceptions(this) { suspend fun checkAndOptionallyDrop(postId: PostId, rating: Rating) {
if (it.value <= toDrop) { if (rating <= toDrop) {
postsRepo.deleteById(it.id) postsRepo.deleteById(postId)
} }
} }
ratingsRepo.getAll().forEach {
runCatchingSafely {
checkAndOptionallyDrop(it.key, it.value)
}
}
ratingsRepo.onNewValue.subscribeSafelyWithoutExceptions(this) {
checkAndOptionallyDrop(it.first, it.second)
}
} }
config.autoclear ?.let { autoclear -> config.autoclear ?.let { autoclear ->
autoclear.autoClearKrontab.toSchedule().asFlow().subscribeSafelyWithoutExceptions(scope) { suspend fun doAutoClear() {
val dropCreatedBefore = it - (autoclear.skipPostAge ?: 0).seconds val dropCreatedBefore = DateTime.now() - (autoclear.skipPostAge ?: 0).seconds
ratingsRepo.getPostsWithRatingLessEq(autoclear.rating).keys.forEach { ratingsRepo.getPostsWithRatingLessEq(autoclear.rating).keys.forEach {
if ((postsRepo.getPostCreationTime(it) ?: return@forEach) < dropCreatedBefore) { if ((postsRepo.getPostCreationTime(it) ?: return@forEach) < dropCreatedBefore) {
postsRepo.deleteById(it) postsRepo.deleteById(it)
} }
} }
} }
runCatchingSafely {
doAutoClear()
}
autoclear.autoClearKrontab.toSchedule().asFlowWithDelays().subscribeSafelyWithoutExceptions(scope) {
doAutoClear()
}
} }
} }
} }

View File

@@ -1,6 +1,7 @@
package dev.inmo.plaguposter.ratings.selector package dev.inmo.plaguposter.ratings.selector
import com.soywiz.klock.DateTime import dev.inmo.micro_utils.repos.KeyValueRepo
import korlibs.time.DateTime
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.plaguposter.ratings.repo.RatingsRepo import dev.inmo.plaguposter.ratings.repo.RatingsRepo
@@ -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): 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, 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

@@ -1,8 +1,13 @@
package dev.inmo.plaguposter.ratings.selector package dev.inmo.plaguposter.ratings.selector
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
interface Selector { interface Selector {
suspend fun take(n: Int = 1, now: DateTime = DateTime.now()): List<PostId> suspend fun take(n: Int = 1, now: DateTime = DateTime.now(), exclude: List<PostId> = emptyList()): List<PostId>
suspend fun takeOneOrNull(now: DateTime = DateTime.now(), exclude: List<PostId> = emptyList()): PostId? = take(
n = 1,
now = now,
exclude = exclude
).firstOrNull()
} }

View File

@@ -1,12 +1,10 @@
package dev.inmo.plaguposter.ratings.selector.models package dev.inmo.plaguposter.ratings.selector.models
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import com.soywiz.klock.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,7 +1,7 @@
package dev.inmo.plaguposter.ratings.selector.models package dev.inmo.plaguposter.ratings.selector.models
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import com.soywiz.klock.Time import korlibs.time.Time
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable

View File

@@ -1,6 +1,6 @@
package dev.inmo.plaguposter.ratings.selector.models package dev.inmo.plaguposter.ratings.selector.models
import com.soywiz.klock.* import korlibs.time.*
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.builtins.serializer import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor

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.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.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(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(SelectorConfig.serializer(), params["selector"] ?: return@single null) } single { get<Json>().decodeFromJsonElement(SelectorConfig.serializer(), params["selector"] ?: return@single null) }
single<Selector> { DefaultSelector(get(), get(), get()) } single<KeyValueRepo<PostId, DateTime>>(qualifier("latestChosenRepo")) {
ExposedKeyValueRepo(
get(),
{ text("post_id") },
{ double("date_time") },
"LatestChosenRepo"
).withMapper(
{ string },
{ unixMillis },
{ PostId(this) },
{ DateTime(this) }
)
}
single<Selector> { DefaultSelector(get(), get(), get(), get(qualifier("latestChosenRepo"))) }
} }
} }

View File

@@ -1,6 +1,6 @@
package dev.inmo.plaguposter.ratings.source.buttons package dev.inmo.plaguposter.ratings.source.buttons
import com.soywiz.klock.DateFormat import korlibs.time.DateFormat
import dev.inmo.kslog.common.TagLogger import dev.inmo.kslog.common.TagLogger
import dev.inmo.kslog.common.d import dev.inmo.kslog.common.d
import dev.inmo.kslog.common.i import dev.inmo.kslog.common.i

View File

@@ -1,15 +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.cache.KeyValueCacheRepo import dev.inmo.micro_utils.repos.MapKeyValueRepo
import dev.inmo.micro_utils.repos.cache.cache.FullKVCache import dev.inmo.micro_utils.repos.cache.cache.FullKVCache
import dev.inmo.micro_utils.repos.cache.full.cached import dev.inmo.micro_utils.repos.cache.full.fullyCached
import dev.inmo.plaguposter.common.ShortMessageInfo import dev.inmo.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.cached(kvCache, scope) ) : PollsToMessagesInfoRepo, KeyValueRepo<PollId, ShortMessageInfo> by repo.fullyCached(kvCache, scope)

View File

@@ -1,15 +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.cached import dev.inmo.micro_utils.repos.cache.full.fullyCached
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
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.cached(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

@@ -37,6 +37,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.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
@@ -129,7 +130,7 @@ object Plugin : Plugin {
content.chatId, content.chatId,
config.ratingOfferText, config.ratingOfferText,
config.variants.keys.toList(), config.variants.keys.toList(),
replyToMessageId = content.messageId 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 +243,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,8 +261,8 @@ 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() 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: InsertStatement<Number>) {
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: InsertStatement<Number>) {
it[keyColumn] = k it[keyColumn] = k.string
} }
} }

View File

@@ -2,7 +2,7 @@ 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.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,7 +11,7 @@ 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) {
override suspend fun getPosts( override suspend fun getPosts(
range: ClosedRange<Rating>, range: ClosedRange<Rating>,

View File

@@ -5,7 +5,6 @@ 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.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.exposed.ExposedRatingsRepo import dev.inmo.plaguposter.ratings.exposed.ExposedRatingsRepo
import dev.inmo.plaguposter.ratings.repo.* import dev.inmo.plaguposter.ratings.repo.*
@@ -14,7 +13,6 @@ import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database 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) { override fun Module.setupDI(database: Database, params: JsonObject) {

View File

@@ -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

@@ -1,4 +1,4 @@
FROM adoptopenjdk/openjdk11 FROM bellsoft/liberica-openjdk-alpine:19
USER 1000 USER 1000

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,76 +0,0 @@
{
"database": {
"url": "jdbc:postgresql://127.0.0.1:8091/test",
"username": "test",
"password": "test",
"driver": "org.postgresql.Driver"
},
"botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
"plugins": [
"dev.inmo.plaguposter.posts.Plugin",
"dev.inmo.plaguposter.posts.registrar.Plugin",
"dev.inmo.plaguposter.ratings.Plugin",
"dev.inmo.plaguposter.ratings.source.Plugin",
"dev.inmo.plaguposter.ratings.selector.Plugin",
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plagubot.plugins.inline.queries.Plugin",
"dev.inmo.plaguposter.triggers.command.Plugin",
"dev.inmo.plaguposter.posts.panel.Plugin",
"dev.inmo.plaguposter.common.CommonPlugin",
"dev.inmo.plaguposter.triggers.timer.Plugin",
"dev.inmo.plaguposter.triggers.timer.disablers.ratings.Plugin",
"dev.inmo.plaguposter.triggers.timer.disablers.autoposts.Plugin"
],
"posts": {
"chats": {
"targetChat": 12345678,
"cacheChat": 12345678,
"sourceChat": 12345678
}
},
"ratingsPolls": {
"variants": {
"Круть": 2,
"Ок": 1,
"Не ок": -1,
"Совсем не ок": -2,
"Посмотреть результаты": 0
},
"autoAttach": true,
"ratingOfferText": "What do you think about it?"
},
"selector": {
"items": [
{
"time": {
"from": "00:00",
"to": "23:59"
},
"rating": {
"prefer": "max"
}
},
{
"time": {
"from": "23:59",
"to": "00:00"
},
"rating": {
"prefer": "max"
}
}
]
},
"timer_trigger": {
"krontab": "0 30 2/4 * *"
},
"panel": {
"textPrefix": "Post management:",
"buttonsPerRow": 2,
"parseMode": "MarkdownV2",
"deleteButtonText": "Delete"
},
"publish_command": {
"panelButtonText": "Publish"
}
}

View File

@@ -14,8 +14,8 @@ function assert_success() {
} }
app=plaguposter app=plaguposter
version="`grep ../gradle.properties -e "^version=" | grep -e "[0-9.]*" -o`" version="`grep ../gradle.properties -e "^version=" | sed -e "s/version=\(.*\)/\1/"`"
server=docker.io/insanusmokrassar server=insanusmokrassar
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

@@ -14,7 +14,7 @@ function assert_success() {
} }
app=plaguposter app=plaguposter
version="`grep ../gradle.properties -e "^version=" | grep -e "[0-9.]*" -o`" version="`grep ../gradle.properties -e "^version=" | sed -e "s/version=\(.*\)/\1/"`"
server=insanusmokrassar server=insanusmokrassar
assert_success ../gradlew build assert_success ../gradlew build

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",
@@ -11,7 +12,6 @@ String[] includes = [
":ratings:gc", ":ratings:gc",
":triggers:command", ":triggers:command",
":triggers:selector_with_timer", ":triggers:selector_with_timer",
":triggers:selector_with_scheduling",
":triggers:timer", ":triggers:timer",
":triggers:timer:disablers:ratings", ":triggers:timer:disablers:ratings",
":triggers:timer:disablers:autoposts", ":triggers:timer:disablers:autoposts",
@@ -30,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

@@ -24,7 +24,7 @@ import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton import dev.inmo.tgbotapi.extensions.utils.types.buttons.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,8 +38,8 @@ 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(
@@ -77,7 +77,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

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

View File

@@ -1,11 +0,0 @@
package dev.inmo.plaguposter.triggers.selector_with_scheduling
import dev.inmo.plagubot.Plugin
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module
object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) {
}
}

View File

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

View File

@@ -1,6 +1,6 @@
package dev.inmo.plaguposter.triggers.selector_with_timer package dev.inmo.plaguposter.triggers.selector_with_timer
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
fun interface AutopostFilter { fun interface AutopostFilter {

View File

@@ -1,14 +1,43 @@
package dev.inmo.plaguposter.triggers.selector_with_timer package dev.inmo.plaguposter.triggers.selector_with_timer
import korlibs.time.DateFormat
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.asFlow import dev.inmo.krontab.utils.asFlowWithDelays
import dev.inmo.krontab.utils.asFlowWithoutDelays
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.koin.singleWithRandomQualifier
import dev.inmo.micro_utils.pagination.FirstPagePagination
import dev.inmo.micro_utils.pagination.Pagination
import dev.inmo.micro_utils.pagination.firstIndex
import dev.inmo.micro_utils.pagination.lastIndexExclusive
import dev.inmo.plagubot.Plugin import dev.inmo.plagubot.Plugin
import dev.inmo.plagubot.plugins.inline.queries.models.Format
import dev.inmo.plagubot.plugins.inline.queries.models.OfferTemplate
import dev.inmo.plagubot.plugins.inline.queries.repos.InlineTemplatesRepo
import dev.inmo.plaguposter.common.ChatConfig
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.repo.ReadPostsRepo
import dev.inmo.plaguposter.posts.sending.PostPublisher import dev.inmo.plaguposter.posts.sending.PostPublisher
import dev.inmo.plaguposter.ratings.selector.Selector import dev.inmo.plaguposter.ratings.selector.Selector
import dev.inmo.tgbotapi.extensions.api.answers.answer
import dev.inmo.tgbotapi.extensions.api.edit.edit
import dev.inmo.tgbotapi.extensions.api.send.send
import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery
import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat
import dev.inmo.tgbotapi.extensions.utils.formatting.makeLinkToMessage
import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.types.buttons.urlButton
import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.utils.row
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectIndexed
import kotlinx.coroutines.flow.take
import kotlinx.serialization.* import kotlinx.serialization.*
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
@@ -16,15 +45,22 @@ import org.koin.core.Koin
import org.koin.core.module.Module import org.koin.core.module.Module
object Plugin : Plugin { object Plugin : Plugin {
@Serializable private const val pageCallbackDataQueryPrefix = "publishing_autoschedule page"
private const val pageCallbackDataQuerySize = 5
@Serializable
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 retryOnPostFailureTimes: Int = 0
) { ) {
@Transient @Transient
val krontab by lazy { val krontab by lazy {
krontabTemplate.toSchedule() krontabTemplate.toSchedule()
} }
@Transient
val format: DateFormat = DateFormat(dateTimeFormat)
} }
override fun Module.setupDI(database: Database, params: JsonObject) { override fun Module.setupDI(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(Config.serializer(), params["timer_trigger"] ?: return@single null) } single { get<Json>().decodeFromJsonElement(Config.serializer(), params["timer_trigger"] ?: return@single null) }
@@ -35,11 +71,116 @@ object Plugin : Plugin {
val publisher = koin.get<PostPublisher>() val publisher = koin.get<PostPublisher>()
val selector = koin.get<Selector>() val selector = koin.get<Selector>()
val filters = koin.getAll<AutopostFilter>().distinct() val filters = koin.getAll<AutopostFilter>().distinct()
koin.get<Config>().krontab.asFlow().subscribeSafelyWithoutExceptions(this) { dateTime -> val chatConfig = koin.get<ChatConfig>()
selector.take(now = dateTime).forEach { postId -> val postsRepo = koin.get<ReadPostsRepo>()
if (filters.all { it.check(postId, dateTime) }) {
publisher.publish(postId) koin.getOrNull<InlineTemplatesRepo>() ?.apply {
addTemplate(
OfferTemplate(
"Autoschedule buttons",
listOf(
Format(
"/autoschedule_panel"
)
),
"Show autoscheduling publishing info"
)
)
}
val krontab = koin.get<Config>().krontab
val retryOnPostFailureTimes = koin.get<Config>().retryOnPostFailureTimes
val dateTimeFormat = koin.get<Config>().format
krontab.asFlowWithDelays().subscribeSafelyWithoutExceptions(this) { dateTime ->
var leftRetries = retryOnPostFailureTimes
do {
val success = runCatching {
selector.takeOneOrNull(now = dateTime) ?.let { postId ->
if (filters.all { it.check(postId, dateTime) }) {
publisher.publish(postId)
} else {
false
}
} ?: false
}.getOrElse {
false
} }
if (success) {
break;
}
leftRetries--;
} while (leftRetries > 0)
}
suspend fun buildPage(pagination: Pagination = FirstPagePagination(size = pageCallbackDataQuerySize)): InlineKeyboardMarkup {
return inlineKeyboard {
row {
if (pagination.page > 1) {
dataButton("⬅️", "${pageCallbackDataQueryPrefix}0")
}
if (pagination.page > 0) {
dataButton("◀️", "${pageCallbackDataQueryPrefix}${pagination.page - 1}")
}
dataButton("\uD83D\uDD04 ${pagination.page}", "${pageCallbackDataQueryPrefix}${pagination.page}")
dataButton("▶️", "${pageCallbackDataQueryPrefix}${pagination.page + 1}")
}
val selected = mutableListOf<PostId>()
krontab.asFlowWithoutDelays().take(pagination.lastIndexExclusive).collectIndexed { i, dateTime ->
val postId = selector.takeOneOrNull(now = dateTime, exclude = selected) ?.also { postId ->
if (filters.all { it.check(postId, dateTime) }) {
selected.add(postId)
} else {
return@collectIndexed
}
}
val post = postsRepo.getFirstMessageInfo(postId ?: return@collectIndexed)
if (i < pagination.firstIndex || post == null) {
return@collectIndexed
}
row {
urlButton(
dateTime.local.format(dateTimeFormat),
makeLinkToMessage(post.chatId, post.messageId)
)
}
}
}
}
onCommand("autoschedule_panel", initialFilter = { chatConfig.allSourceChatIds.any { chatId -> it.sameChat(chatId) } }) {
val keyboard = buildPage()
runCatchingSafely {
edit(it, replyMarkup = keyboard) {
+"Your schedule:"
}
}.onFailure { _ ->
send(it.chat, replyMarkup = keyboard) {
+"Your schedule:"
}
}
}
onMessageDataCallbackQuery(
Regex("^$pageCallbackDataQueryPrefix\\d+"),
initialFilter = { chatConfig.allSourceChatIds.any { sourceChatId -> it.message.sameChat(sourceChatId) } }
) {
val page = it.data.removePrefix(pageCallbackDataQueryPrefix).toIntOrNull() ?: let { _ ->
answer(it)
return@onMessageDataCallbackQuery
}
runCatchingSafely {
edit(
it.message,
replyMarkup = buildPage(Pagination(page, size = pageCallbackDataQuerySize))
)
}.onFailure { _ ->
answer(it)
} }
} }
} }

View File

@@ -1,10 +1,10 @@
package dev.inmo.plaguposter.triggers.timer package dev.inmo.plaguposter.triggers.timer
import com.soywiz.klock.DateFormat import korlibs.time.DateFormat
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import com.soywiz.klock.DateTimeTz import korlibs.time.DateTimeTz
import com.soywiz.klock.Month import korlibs.time.Month
import com.soywiz.klock.Year import korlibs.time.Year
import dev.inmo.micro_utils.coroutines.runCatchingSafely import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.repos.unset import dev.inmo.micro_utils.repos.unset
import dev.inmo.plaguposter.common.SuccessfulSymbol import dev.inmo.plaguposter.common.SuccessfulSymbol

View File

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

View File

@@ -1,12 +1,13 @@
package dev.inmo.plaguposter.triggers.timer package dev.inmo.plaguposter.triggers.timer
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.plus import dev.inmo.micro_utils.coroutines.plus
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions 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,6 +1,6 @@
package dev.inmo.plaguposter.triggers.timer package dev.inmo.plaguposter.triggers.timer
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.micro_utils.repos.KeyValueRepo import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId

View File

@@ -1,6 +1,6 @@
package dev.inmo.plaguposter.triggers.timer package dev.inmo.plaguposter.triggers.timer
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.micro_utils.coroutines.runCatchingSafely import dev.inmo.micro_utils.coroutines.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

View File

@@ -1,6 +1,6 @@
package dev.inmo.plaguposter.triggers.timer.repo package dev.inmo.plaguposter.triggers.timer.repo
import com.soywiz.klock.DateTime import korlibs.time.DateTime
import dev.inmo.micro_utils.common.firstNotNull import dev.inmo.micro_utils.common.firstNotNull
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.pagination.paginate import dev.inmo.micro_utils.pagination.paginate