11 Commits

18 changed files with 143 additions and 55 deletions

View File

@@ -1,5 +1,17 @@
# PlaguPoster
## 0.5.3
* Dependencies update
## 0.5.2
* Dependencies update
## 0.5.1
* Add opportunity to set unique
## 0.5.0
* Dependencies update

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ object CommonPlugin : Plugin {
val config = koin.get<ChatConfig>()
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)}" }
}
}

View File

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

View File

@@ -1,14 +1,14 @@
[versions]
kotlin = "1.9.20"
kotlin-serialization = "1.6.0"
kotlin = "1.9.22"
kotlin-serialization = "1.6.2"
plagubot = "7.3.0"
tgbotapi = "9.3.0"
microutils = "0.20.12"
kslog = "1.2.4"
krontab = "2.2.3"
plagubot-plugins = "0.16.0"
plagubot = "8.1.1"
tgbotapi = "10.0.1"
microutils = "0.20.34"
kslog = "1.3.2"
krontab = "2.2.7"
plagubot-plugins = "0.18.1"
dokka = "1.9.10"

View File

@@ -11,6 +11,9 @@ 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
@@ -20,7 +23,10 @@ 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
@@ -98,10 +104,12 @@ object Plugin : Plugin {
return@forEach
}
send(
chatsConfig.sourceChatId,
"Can't find any messages for post $postId. So, deleting it"
)
runCatching {
send(
chatsConfig.cacheChatId,
"Can't find any messages for post $postId. So, deleting it"
)
}
runCatching {
postsRepo.deleteById(postId)
}
@@ -147,9 +155,18 @@ object Plugin : Plugin {
}
)
val answer = waitMessageDataCallbackQuery().filter {
it.message.sameMessage(message)
}.first()
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) {
@@ -164,5 +181,14 @@ object Plugin : Plugin {
}
}
}
koin.getOrNull<InlineTemplatesRepo>() ?.addTemplate(
OfferTemplate(
"Force posts check",
listOf(
Format("/force_garbage_collection")
),
"Force check posts without exists messages"
)
)
}
}

View File

@@ -31,6 +31,7 @@ import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton
import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard
import dev.inmo.tgbotapi.types.IdChatIdentifier
import dev.inmo.tgbotapi.types.MessageIdentifier
import dev.inmo.tgbotapi.types.ReplyParameters
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup
import dev.inmo.tgbotapi.types.message.ParseMode
@@ -112,7 +113,7 @@ object Plugin : Plugin {
firstContent.chatId,
text = config.text,
parseMode = config.parseMode,
replyToMessageId = firstContent.messageId,
replyParameters = ReplyParameters(firstContent.chatId, firstContent.messageId),
replyMarkup = InlineKeyboardMarkup(buttons),
disableNotification = true
).also { sentMessage ->
@@ -146,7 +147,7 @@ object Plugin : Plugin {
onMessageDataCallbackQuery (
initialFilter = {
it.data.startsWith(PanelButtonsAPI.openGlobalMenuDataPrefix) && it.message.chat.id == chatsConfig.sourceChatId
it.data.startsWith(PanelButtonsAPI.openGlobalMenuDataPrefix) && it.message.chat.id in chatsConfig.allSourceChatIds
}
) {
val postId = it.data.removePrefix(PanelButtonsAPI.openGlobalMenuDataPrefix).let(::PostId)
@@ -155,7 +156,7 @@ object Plugin : Plugin {
}
onMessageDataCallbackQuery(
initialFilter = {
it.data.startsWith("delete ") && it.message.chat.id == chatsConfig.sourceChatId
it.data.startsWith("delete ") && it.message.chat.id in chatsConfig.allSourceChatIds
}
) { query ->
val postId = query.data.removePrefix("delete ").let(::PostId)
@@ -182,7 +183,7 @@ object Plugin : Plugin {
}
onMessageDataCallbackQuery(
initialFilter = {
it.data.startsWith("refresh ") && it.message.chat.id == chatsConfig.sourceChatId
it.data.startsWith("refresh ") && it.message.chat.id in chatsConfig.allSourceChatIds
}
) { query ->
val postId = query.data.removePrefix("refresh ").let(::PostId)

View File

@@ -58,7 +58,7 @@ object Plugin : Plugin {
}
single {
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

@@ -119,12 +119,12 @@ object Plugin : Plugin {
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()))
}
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)))
}

View File

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

View File

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

View File

@@ -1,14 +1,33 @@
package dev.inmo.plaguposter.ratings.selector
import dev.inmo.micro_utils.repos.KeyValueRepo
import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo
import dev.inmo.micro_utils.repos.mappers.withMapper
import dev.inmo.plagubot.Plugin
import dev.inmo.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.ratings.selector.models.SelectorConfig
import korlibs.time.DateTime
import kotlinx.serialization.json.*
import org.jetbrains.exposed.sql.Database
import org.koin.core.module.Module
import org.koin.core.qualifier.qualifier
object Plugin : Plugin {
override fun Module.setupDI(database: Database, params: JsonObject) {
single { get<Json>().decodeFromJsonElement(SelectorConfig.serializer(), params["selector"] ?: return@single null) }
single<Selector> { DefaultSelector(get(), get(), get()) }
single<KeyValueRepo<PostId, DateTime>>(qualifier("latestChosenRepo")) {
ExposedKeyValueRepo(
get(),
{ text("post_id") },
{ double("date_time") },
"LatestChosenRepo"
).withMapper(
{ string },
{ unixMillis },
{ PostId(this) },
{ DateTime(this) }
)
}
single<Selector> { DefaultSelector(get(), get(), get(), get(qualifier("latestChosenRepo"))) }
}
}

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.flatInlineKeyboard
import dev.inmo.tgbotapi.extensions.utils.types.buttons.inlineKeyboard
import dev.inmo.tgbotapi.types.ReplyParameters
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.message.textsources.bold
import dev.inmo.tgbotapi.types.message.textsources.regular
@@ -129,7 +130,7 @@ object Plugin : Plugin {
content.chatId,
config.ratingOfferText,
config.variants.keys.toList(),
replyToMessageId = content.messageId
replyParameters = ReplyParameters(content.chatId, content.messageId)
)
pollsToPostsIdsRepo.set(sent.content.poll.id, postId)
pollsToMessageInfoRepo.set(sent.content.poll.id, sent.short())
@@ -242,7 +243,7 @@ object Plugin : Plugin {
}
}
onCommand("ratings", requireOnlyCommandInMessage = true) {
if (it.chat.id == chatConfig.sourceChatId) {
if (it.chat.id in chatConfig.allSourceChatIds) {
val ratings = ratingsRepo.postsByRatings().toList().sortedByDescending { it.first }
val textSources = buildEntities {
+ "Ratings amount: " + bold("${ratings.sumOf { it.second.size }}") + "\n\n"
@@ -260,8 +261,8 @@ object Plugin : Plugin {
}
}
}
includeRootNavigationButtonsHandler(setOf(chatConfig.sourceChatId), ratingsRepo, postsRepo)
onMessageDataCallbackQuery("ratings_interactive", initialFilter = { it.message.chat.id == chatConfig.sourceChatId }) {
includeRootNavigationButtonsHandler(chatConfig.allSourceChatIds, ratingsRepo, postsRepo)
onMessageDataCallbackQuery("ratings_interactive", initialFilter = { it.message.chat.id in chatConfig.allSourceChatIds }) {
edit(
it.message,
ratingsRepo.buildRootButtons()

View File

@@ -51,7 +51,8 @@
"to": "23:59"
},
"rating": {
"prefer": "max"
"prefer": "max",
"uniqueCount": 1
}
},
{
@@ -60,7 +61,8 @@
"to": "00:00"
},
"rating": {
"prefer": "max"
"prefer": "max",
"uniqueCount": 1
}
}
]

View File

@@ -11,12 +11,14 @@ services:
POSTGRES_DB: "test"
volumes:
- "./db/:/var/lib/postgresql/data"
- "/etc/timezone:/etc/timezone:ro"
plaguposter:
image: insanusmokrassar/plaguposter:latest
container_name: "plaguposter"
restart: "unless-stopped"
volumes:
- "./config.json:/config.json"
- "/etc/timezone:/etc/timezone:ro"
depends_on:
- "plaguposter_postgres"
links:

View File

@@ -151,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()
runCatchingSafely {
@@ -167,7 +167,7 @@ object Plugin : Plugin {
onMessageDataCallbackQuery(
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 { _ ->
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.plaguposter.posts.models.PostId
import dev.inmo.plaguposter.posts.sending.PostPublisher
import korlibs.time.millisecondsLong
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay