39 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
48 changed files with 562 additions and 348 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

@@ -11,6 +11,9 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 17
- name: Rewrite version - name: Rewrite version
run: | run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"

View File

@@ -1,5 +1,37 @@
# 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 ## 0.2.3
* Add opportunity to use several target chat ids * Add opportunity to use several target chat ids

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ object CommonPlugin : Plugin {
val config = koin.get<ChatConfig>() val config = koin.get<ChatConfig>()
Log.iS { "Target chats info: ${config.allTargetChatIds.map { getChat(it) }.joinToString()}" } Log.iS { "Target chats info: ${config.allTargetChatIds.map { getChat(it) }.joinToString()}" }
Log.iS { "Source chat info: ${getChat(config.sourceChatId)}" } Log.iS { "Source chats info: ${config.allSourceChatIds.map { getChat(it) }.joinToString()}" }
Log.iS { "Cache chat info: ${getChat(config.cacheChatId)}" } Log.iS { "Cache chat info: ${getChat(config.cacheChatId)}" }
} }
} }

View File

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

View File

@@ -1,16 +1,16 @@
[versions] [versions]
kotlin = "1.8.22" kotlin = "1.9.23"
kotlin-serialization = "1.5.1" kotlin-serialization = "1.6.3"
plagubot = "7.0.0" plagubot = "8.3.0"
tgbotapi = "9.0.0" tgbotapi = "12.0.1"
microutils = "0.19.9" microutils = "0.20.45"
kslog = "1.1.2" kslog = "1.3.3"
krontab = "2.1.2" krontab = "2.2.9"
plagubot-plugins = "0.13.0" plagubot-plugins = "0.18.3"
dokka = "1.8.20" dokka = "1.9.20"
psql = "42.6.0" psql = "42.6.0"

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.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
} }

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -58,7 +58,7 @@ object Plugin : Plugin {
} }
single { single {
val config = get<Config>() val config = get<Config>()
PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.allTargetChatIds, config.deleteAfterPublishing) PostPublisher(get(), get(), config.chats.cacheChatId, config.chats.allTargetChatIds.toList(), config.deleteAfterPublishing)
} }
} }

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import dev.inmo.plaguposter.posts.models.*
import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.repo.PostsRepo
import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.ChatId
import dev.inmo.tgbotapi.types.IdChatIdentifier import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier import dev.inmo.tgbotapi.types.MessageId
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
@@ -26,6 +26,7 @@ class ExposedPostsRepo(
) { ) {
val idColumn = text("id") val idColumn = text("id")
val createdColumn = double("datetime").default(0.0) val createdColumn = double("datetime").default(0.0)
val latestUpdateColumn = double("latest_update").default(0.0)
private val contentRepo by lazy { private val contentRepo by lazy {
ExposedContentInfoRepo( ExposedContentInfoRepo(
@@ -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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,6 @@ package dev.inmo.plaguposter.ratings.source.repos
import dev.inmo.micro_utils.repos.KeyValueRepo import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.plaguposter.common.ShortMessageInfo import dev.inmo.plaguposter.common.ShortMessageInfo
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.types.PollIdentifier import dev.inmo.tgbotapi.types.PollId
interface PollsToMessagesInfoRepo : KeyValueRepo<PollIdentifier, ShortMessageInfo> interface PollsToMessagesInfoRepo : KeyValueRepo<PollId, ShortMessageInfo>

View File

@@ -2,6 +2,6 @@ package dev.inmo.plaguposter.ratings.source.repos
import dev.inmo.micro_utils.repos.KeyValueRepo import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.plaguposter.posts.models.PostId import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.tgbotapi.types.PollIdentifier import dev.inmo.tgbotapi.types.PollId
interface PollsToPostsIdsRepo : KeyValueRepo<PollIdentifier, PostId> interface PollsToPostsIdsRepo : KeyValueRepo<PollId, PostId>

View File

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

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

View File

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

View File

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

View File

@@ -1,131 +1,85 @@
{ {
"database": { "database": {
"url": "jdbc:postgresql://postgres/test_db", "url": "jdbc:postgresql://postgres:5432/test",
"username": "test_user", "username": "test",
"password": "test_password", "password": "test",
"driver": "org.postgresql.Driver" "driver": "org.postgresql.Driver"
}, },
"botToken": "1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi", "botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678",
"plugins": [ "plugins": [
"dev.inmo.plaguposter.posts.Plugin", "dev.inmo.plaguposter.posts.Plugin",
"dev.inmo.plaguposter.posts.registrar.Plugin", "dev.inmo.plaguposter.posts.registrar.Plugin",
"dev.inmo.plaguposter.ratings.Plugin", "dev.inmo.plaguposter.ratings.Plugin",
"dev.inmo.plaguposter.ratings.source.Plugin", "dev.inmo.plaguposter.ratings.source.Plugin",
"dev.inmo.plaguposter.ratings.selector.Plugin", "dev.inmo.plaguposter.ratings.selector.Plugin",
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plaguposter.ratings.gc.Plugin", "dev.inmo.plaguposter.ratings.gc.Plugin",
"dev.inmo.plaguposter.triggers.selector_with_timer.Plugin",
"dev.inmo.plagubot.plugins.inline.queries.Plugin", "dev.inmo.plagubot.plugins.inline.queries.Plugin",
"dev.inmo.plaguposter.triggers.command.Plugin" "dev.inmo.plaguposter.triggers.command.Plugin",
"dev.inmo.plaguposter.posts.panel.Plugin",
"dev.inmo.plaguposter.common.CommonPlugin",
"dev.inmo.plaguposter.triggers.timer.Plugin",
"dev.inmo.plaguposter.triggers.timer.disablers.ratings.Plugin",
"dev.inmo.plaguposter.triggers.timer.disablers.autoposts.Plugin",
"dev.inmo.plaguposter.posts.gc.Plugin"
], ],
"posts": { "posts": {
"chats": { "chats": {
"targetChat": -1001234567890, "targetChat": 12345678,
"cacheChat": -1001234567890, "cacheChat": 12345678,
"sourceChat": -1001234567890 "sourceChat": 12345678,
}, "targetChats": [12345678],
"autoRemoveMessages": true "_note": "You must set targetChat or targetChats with at least one object"
}
}, },
"ratingsPolls": { "ratingsPolls": {
"variants": { "variants": {
"Круть": 2, "Cool": 2,
"Ок": 1, "Ok": 1,
"Не ок": -1, "Not ok": -1,
"Совсем не ок": -2, "Inappropriate": -2,
"Посмотреть результаты": 0 "Results": 0
}, },
"autoAttach": true, "autoAttach": true,
"ratingOfferText": "How do you like it?" "ratingOfferText": "What do you think about it?"
}, },
"selector": { "selector": {
"items": [ "items": [
{ {
"time": { "time": {
"from": "23:00", "from": "00:00",
"to": "23:59" "to": "23:59"
}, },
"rating": { "rating": {
"min": -1.0,
"max": 2.0,
"prefer": "max", "prefer": "max",
"otherwise": { "uniqueCount": 1
"rating": {
"min": 2.0,
"prefer": "min",
"postAge": 86400
}
},
"postAge": 86400
} }
}, },
{ {
"time": { "time": {
"from": "00:00", "from": "23:59",
"to": "06:59" "to": "00:00"
},
"rating": {
"min": -1.0,
"max": 2.0,
"prefer": "max",
"otherwise": {
"rating": {
"min": 2.0,
"prefer": "min",
"postAge": 86400
}
},
"postAge": 86400
}
},
{
"time": {
"from": "07:00",
"to": "12:00"
},
"rating": {
"min": 1.0,
"prefer": "min",
"otherwise": {
"rating": {
"max": 1.0,
"prefer": "max",
"postAge": 86400
}
},
"postAge": 86400
}
},
{
"time": {
"from": "12:00",
"to": "16:00"
},
"rating": {
"min": 2.0,
"prefer": "min",
"otherwise": {
"rating": {
"max": 2.0,
"prefer": "max",
"postAge": 86400
}
},
"postAge": 86400
}
},
{
"time": {
"from": "16:00",
"to": "23:00"
}, },
"rating": { "rating": {
"prefer": "max", "prefer": "max",
"postAge": 86400 "uniqueCount": 1
} }
} }
] ]
}, },
"timer_trigger": { "timer_trigger": {
"krontab": "0 30 */5 * *" "krontab": "0 30 2/4 * *",
"retryOnPostFailureTimes": 0,
"_note": "retryOnPostFailureTimes will retry to publish one or several posts if posting has been failed"
},
"panel": {
"textPrefix": "Post management:",
"buttonsPerRow": 2,
"parseMode": "MarkdownV2",
"deleteButtonText": "Delete"
},
"publish_command": {
"panelButtonText": "Publish"
}, },
"gc": { "gc": {
"autoclear": { "autoclear": {
@@ -133,6 +87,11 @@
"autoClearKrontab": "0 0 0 * *", "autoClearKrontab": "0 0 0 * *",
"skipPostAge": 86400 "skipPostAge": 86400
}, },
"immediateDrop": -2 "immediateDrop": -6
},
"messagesChecker": {
"krontab": "0 0 0 * *",
"throttlingMillis": 1000,
"doFullCheck": false
} }
} }

View File

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

View File

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

View File

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

@@ -51,7 +51,8 @@ object Plugin : Plugin {
internal data class Config( internal data class Config(
@SerialName("krontab") @SerialName("krontab")
val krontabTemplate: KrontabTemplate, val krontabTemplate: KrontabTemplate,
val dateTimeFormat: String = "HH:mm:ss, dd.MM.yyyy" val dateTimeFormat: String = "HH:mm:ss, dd.MM.yyyy",
val retryOnPostFailureTimes: Int = 0
) { ) {
@Transient @Transient
val krontab by lazy { val krontab by lazy {
@@ -88,13 +89,27 @@ object Plugin : Plugin {
} }
val krontab = koin.get<Config>().krontab val krontab = koin.get<Config>().krontab
val retryOnPostFailureTimes = koin.get<Config>().retryOnPostFailureTimes
val dateTimeFormat = koin.get<Config>().format val dateTimeFormat = koin.get<Config>().format
krontab.asFlowWithDelays().subscribeSafelyWithoutExceptions(this) { dateTime -> krontab.asFlowWithDelays().subscribeSafelyWithoutExceptions(this) { dateTime ->
selector.take(now = dateTime).forEach { postId -> var leftRetries = retryOnPostFailureTimes
if (filters.all { it.check(postId, dateTime) }) { do {
publisher.publish(postId) 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 { suspend fun buildPage(pagination: Pagination = FirstPagePagination(size = pageCallbackDataQuerySize)): InlineKeyboardMarkup {
@@ -113,7 +128,7 @@ object Plugin : Plugin {
val selected = mutableListOf<PostId>() val selected = mutableListOf<PostId>()
krontab.asFlowWithoutDelays().take(pagination.lastIndexExclusive).collectIndexed { i, dateTime -> krontab.asFlowWithoutDelays().take(pagination.lastIndexExclusive).collectIndexed { i, dateTime ->
val postId = selector.take(now = dateTime, exclude = selected).firstOrNull() ?.also { postId -> val postId = selector.takeOneOrNull(now = dateTime, exclude = selected) ?.also { postId ->
if (filters.all { it.check(postId, dateTime) }) { if (filters.all { it.check(postId, dateTime) }) {
selected.add(postId) selected.add(postId)
} else { } else {
@@ -136,7 +151,7 @@ object Plugin : Plugin {
} }
} }
onCommand("autoschedule_panel", initialFilter = { it.sameChat(chatConfig.sourceChatId) }) { onCommand("autoschedule_panel", initialFilter = { chatConfig.allSourceChatIds.any { chatId -> it.sameChat(chatId) } }) {
val keyboard = buildPage() val keyboard = buildPage()
runCatchingSafely { runCatchingSafely {
@@ -152,7 +167,7 @@ object Plugin : Plugin {
onMessageDataCallbackQuery( onMessageDataCallbackQuery(
Regex("^$pageCallbackDataQueryPrefix\\d+"), Regex("^$pageCallbackDataQueryPrefix\\d+"),
initialFilter = { it.message.sameChat(chatConfig.sourceChatId) } initialFilter = { chatConfig.allSourceChatIds.any { sourceChatId -> it.message.sameChat(sourceChatId) } }
) { ) {
val page = it.data.removePrefix(pageCallbackDataQueryPrefix).toIntOrNull() ?: let { _ -> val page = it.data.removePrefix(pageCallbackDataQueryPrefix).toIntOrNull() ?: let { _ ->
answer(it) answer(it)

View File

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