diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c36b38..712cfa7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,9 +5,9 @@ kotlin-serialization = "1.4.0" plagubot = "2.3.1" tgbotapi = "3.2.1" -microutils = "0.12.11" -kslog = "0.5.1" -krontab = "0.8.0" +microutils = "0.12.13" +kslog = "0.5.2" +krontab = "0.8.1" tgbotapi-libraries = "0.5.3" psql = "42.3.6" @@ -39,6 +39,7 @@ plagubot-bot = { module = "dev.inmo:plagubot.bot", version.ref = "plagubot" } microutils-repos-common = { module = "dev.inmo:micro_utils.repos.common", version.ref = "microutils" } microutils-repos-exposed = { module = "dev.inmo:micro_utils.repos.exposed", version.ref = "microutils" } microutils-repos-cache = { module = "dev.inmo:micro_utils.repos.cache", version.ref = "microutils" } +microutils-koin = { module = "dev.inmo:micro_utils.koin", version.ref = "microutils" } kslog = { module = "dev.inmo:kslog", version.ref = "kslog" } krontab = { module = "dev.inmo:krontab", version.ref = "krontab" } diff --git a/posts/panel/build.gradle b/posts/panel/build.gradle index 779ca41..3595568 100644 --- a/posts/panel/build.gradle +++ b/posts/panel/build.gradle @@ -12,10 +12,7 @@ kotlin { dependencies { api project(":plaguposter.common") api project(":plaguposter.posts") - } - } - jvmMain { - dependencies { + api libs.microutils.koin } } } diff --git a/posts/panel/src/commonMain/kotlin/PanelButtonBuilder.kt b/posts/panel/src/commonMain/kotlin/PanelButtonBuilder.kt new file mode 100644 index 0000000..74960f0 --- /dev/null +++ b/posts/panel/src/commonMain/kotlin/PanelButtonBuilder.kt @@ -0,0 +1,8 @@ +package dev.inmo.plaguposter.posts.panel + +import dev.inmo.plaguposter.posts.models.RegisteredPost +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.InlineKeyboardButton + +fun interface PanelButtonBuilder { + suspend fun buildButton(post: RegisteredPost): InlineKeyboardButton? +} diff --git a/posts/panel/src/commonMain/kotlin/PanelButtonSettings.kt b/posts/panel/src/commonMain/kotlin/PanelButtonSettings.kt deleted file mode 100644 index 38b24e5..0000000 --- a/posts/panel/src/commonMain/kotlin/PanelButtonSettings.kt +++ /dev/null @@ -1,9 +0,0 @@ -package dev.inmo.plaguposter.posts.panel - -import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.InlineKeyboardButton -import kotlinx.serialization.Serializable - -@Serializable -data class PanelButtonSettings( - val button: InlineKeyboardButton -) diff --git a/posts/panel/src/commonMain/kotlin/PanelButtonsAPI.kt b/posts/panel/src/commonMain/kotlin/PanelButtonsAPI.kt new file mode 100644 index 0000000..9c9aa32 --- /dev/null +++ b/posts/panel/src/commonMain/kotlin/PanelButtonsAPI.kt @@ -0,0 +1,29 @@ +package dev.inmo.plaguposter.posts.panel + +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton + +class PanelButtonsAPI( + private val preset: List, + private val rootPanelButtonText: String +) { + private val _buttons = mutableSetOf().also { + it.addAll(preset) + } + internal val buttonsBuilders: List + get() = _buttons.toList() + + val RootPanelButtonBuilder = PanelButtonBuilder { + CallbackDataInlineKeyboardButton( + rootPanelButtonText, + "$openGlobalMenuDataPrefix${it.id.string}" + ) + } + + fun add(button: PanelButtonBuilder) = _buttons.add(button) + fun remove(button: PanelButtonBuilder) = _buttons.remove(button) + + companion object { + internal const val openGlobalMenuData = "force_refresh_panel" + internal const val openGlobalMenuDataPrefix = "$openGlobalMenuData " + } +} diff --git a/posts/panel/src/jvmMain/kotlin/Plugin.kt b/posts/panel/src/jvmMain/kotlin/Plugin.kt index c4f449e..e4bff36 100644 --- a/posts/panel/src/jvmMain/kotlin/Plugin.kt +++ b/posts/panel/src/jvmMain/kotlin/Plugin.kt @@ -1,8 +1,31 @@ package dev.inmo.plaguposter.posts.panel +import com.benasher44.uuid.uuid4 +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions +import dev.inmo.micro_utils.koin.getAllDistinct +import dev.inmo.micro_utils.repos.deleteById +import dev.inmo.micro_utils.repos.set import dev.inmo.plagubot.Plugin +import dev.inmo.plaguposter.common.ChatConfig +import dev.inmo.plaguposter.posts.models.PostId +import dev.inmo.plaguposter.posts.panel.repos.PostsMessages +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.send.send import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext -import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.InlineKeyboardButton +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery +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.withContentOrNull +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup +import dev.inmo.tgbotapi.types.message.ParseMode +import dev.inmo.tgbotapi.types.message.content.TextContent +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.serialization.Serializable import kotlinx.serialization.json.* import org.jetbrains.exposed.sql.Database @@ -12,15 +35,108 @@ import org.koin.core.module.Module object Plugin : Plugin { @Serializable internal data class Config ( - val text: String = "You have registered new post with %s posts", - val buttonsPrefix: String = ". Here the buttons available for management of post:", - val preset: List>? = null + val text: String = "Post settings:", + val parseMode: ParseMode? = null, + val buttonsPerRow: Int = 4, + val deleteButtonText: String? = null, + val rootButtonText: String = "Return to panel" ) override fun Module.setupDI(database: Database, params: JsonObject) { - single { } + params["panel"] ?.let { element -> + single { get().decodeFromJsonElement(Config.serializer(), element) } + } + single { + val config = getOrNull() ?: Config() + val builtInButtons = listOfNotNull( + config.deleteButtonText ?.let { text -> + PanelButtonBuilder { + CallbackDataInlineKeyboardButton( + text, + "delete ${it.id.string}" + ) + } + } + ) + PanelButtonsAPI( + getAllDistinct() + builtInButtons, + config.rootButtonText + ) + } } override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { - TODO("Not yet implemented") + val postsRepo = koin.get() + val chatsConfig = koin.get() + val config = koin.getOrNull() ?: Config() + val keeper = koin.get() + val postsMessages = PostsMessages(koin.get(), koin.get()) + + postsRepo.newObjectsFlow.subscribeSafelyWithoutExceptions(this) { + val firstContent = it.content.first() + val buttons = keeper.buttonsBuilders.chunked(config.buttonsPerRow).mapNotNull { row -> + row.mapNotNull { builder -> + builder.buildButton(it) + }.takeIf { it.isNotEmpty() } + } + send( + firstContent.chatId, + text = config.text, + parseMode = config.parseMode, + replyToMessageId = firstContent.messageId, + replyMarkup = InlineKeyboardMarkup(buttons), + disableNotification = true + ).also { sentMessage -> + postsMessages.set(it.id, sentMessage.chat.id to sentMessage.messageId) + } + } + postsRepo.deletedObjectsIdsFlow.subscribeSafelyWithoutExceptions(this) { + val (chatId, messageId) = postsMessages.get(it) ?: return@subscribeSafelyWithoutExceptions + + delete(chatId, messageId) + } + + onMessageDataCallbackQuery ( + initialFilter = { + it.data.startsWith(PanelButtonsAPI.openGlobalMenuDataPrefix) && it.message.chat.id == chatsConfig.sourceChatId + } + ) { + val postId = it.data.removePrefix(PanelButtonsAPI.openGlobalMenuDataPrefix).let(::PostId) + val post = postsRepo.getById(postId) ?: return@onMessageDataCallbackQuery + val buttons = keeper.buttonsBuilders.chunked(config.buttonsPerRow).mapNotNull { row -> + row.mapNotNull { builder -> + builder.buildButton(post) + }.takeIf { it.isNotEmpty() } + } + edit( + it.message.withContentOrNull() ?: return@onMessageDataCallbackQuery, + replyMarkup = InlineKeyboardMarkup(buttons) + ) + } + onMessageDataCallbackQuery( + initialFilter = { + it.data.startsWith("delete ") && it.message.chat.id == chatsConfig.sourceChatId + } + ) { query -> + val postId = query.data.removePrefix("delete ").let(::PostId) + val post = postsRepo.getById(postId) ?: return@onMessageDataCallbackQuery + + val approveData = uuid4().toString() + + edit( + query.message, + replyMarkup = flatInlineKeyboard { + dataButton("\uD83D\uDDD1", approveData) + keeper.RootPanelButtonBuilder.buildButton(post) ?.let(::add) + } + ) + + val pushedButton = waitMessageDataCallbackQuery().first { + it.message.sameMessage(query.message) + } + + if (pushedButton.data == approveData) { + postsRepo.deleteById(postId) + } + } } } diff --git a/posts/panel/src/jvmMain/kotlin/repos/PostsMessages.kt b/posts/panel/src/jvmMain/kotlin/repos/PostsMessages.kt new file mode 100644 index 0000000..984a6e2 --- /dev/null +++ b/posts/panel/src/jvmMain/kotlin/repos/PostsMessages.kt @@ -0,0 +1,29 @@ +package dev.inmo.plaguposter.posts.panel.repos + +import dev.inmo.micro_utils.repos.KeyValueRepo +import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo +import dev.inmo.micro_utils.repos.mappers.withMapper +import dev.inmo.plaguposter.posts.models.PostId +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.MessageIdentifier +import kotlinx.serialization.builtins.PairSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import org.jetbrains.exposed.sql.Database + +private val ChatIdToMessageSerializer = PairSerializer(ChatId.serializer(), MessageIdentifier.serializer()) + +fun PostsMessages( + database: Database, + json: Json +): KeyValueRepo> = ExposedKeyValueRepo( + database, + { text("postId") }, + { text("chatToMessage") }, + "panel_messages_info" +).withMapper( + { string }, + { json.encodeToString(ChatIdToMessageSerializer, this) }, + { PostId(this) }, + { json.decodeFromString(ChatIdToMessageSerializer, this) } +) diff --git a/runner/build.gradle b/runner/build.gradle index 466312e..d7cf67f 100644 --- a/runner/build.gradle +++ b/runner/build.gradle @@ -11,6 +11,7 @@ dependencies { api libs.plagubot.bot api project(":plaguposter.posts") + api project(":plaguposter.posts.panel") api project(":plaguposter.posts_registrar") api project(":plaguposter.triggers.command") api project(":plaguposter.triggers.selector_with_timer")