diff --git a/gradle.properties b/gradle.properties index dbcb044..e1644be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,5 +10,5 @@ android.enableJetifier=true # Project data group=dev.inmo -version=0.0.1 -android_code_version=1 +version=0.0.2 +android_code_version=2 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 new file mode 100644 index 0000000..3595568 --- /dev/null +++ b/posts/panel/build.gradle @@ -0,0 +1,19 @@ +plugins { + id "org.jetbrains.kotlin.multiplatform" + id "org.jetbrains.kotlin.plugin.serialization" + id "com.android.library" +} + +apply from: "$mppProjectWithSerializationPresetPath" + +kotlin { + sourceSets { + commonMain { + dependencies { + api project(":plaguposter.common") + api project(":plaguposter.posts") + api libs.microutils.koin + } + } + } +} diff --git a/posts/panel/src/commonMain/kotlin/PackageInfo.kt b/posts/panel/src/commonMain/kotlin/PackageInfo.kt new file mode 100644 index 0000000..00707d7 --- /dev/null +++ b/posts/panel/src/commonMain/kotlin/PackageInfo.kt @@ -0,0 +1 @@ +package dev.inmo.plaguposter.posts.panel 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/PanelButtonsAPI.kt b/posts/panel/src/commonMain/kotlin/PanelButtonsAPI.kt new file mode 100644 index 0000000..2f8c11d --- /dev/null +++ b/posts/panel/src/commonMain/kotlin/PanelButtonsAPI.kt @@ -0,0 +1,35 @@ +package dev.inmo.plaguposter.posts.panel + +import dev.inmo.plaguposter.posts.models.PostId +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton +import kotlinx.coroutines.flow.MutableSharedFlow + +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() + internal val forceRefreshFlow = MutableSharedFlow() + + val RootPanelButtonBuilder = PanelButtonBuilder { + CallbackDataInlineKeyboardButton( + rootPanelButtonText, + "$openGlobalMenuDataPrefix${it.id.string}" + ) + } + + fun add(button: PanelButtonBuilder) = _buttons.add(button) + fun remove(button: PanelButtonBuilder) = _buttons.remove(button) + suspend fun forceRefresh(postId: PostId) { + forceRefreshFlow.emit(postId) + } + + 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 new file mode 100644 index 0000000..d06b18a --- /dev/null +++ b/posts/panel/src/jvmMain/kotlin/Plugin.kt @@ -0,0 +1,158 @@ +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.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.ChatId +import dev.inmo.tgbotapi.types.MessageIdentifier +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.abstracts.Message +import dev.inmo.tgbotapi.types.message.content.TextContent +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 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) { + 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) { + val postsRepo = koin.get() + val chatsConfig = koin.get() + val config = koin.getOrNull() ?: Config() + val api = koin.get() + val postsMessages = PostsMessages(koin.get(), koin.get()) + + postsRepo.newObjectsFlow.subscribeSafelyWithoutExceptions(this) { + val firstContent = it.content.first() + val buttons = api.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) + } + + suspend fun updatePost( + postId: PostId, + chatId: ChatId, + messageId: MessageIdentifier + ) { + val post = postsRepo.getById(postId) ?: return + val buttons = api.buttonsBuilders.chunked(config.buttonsPerRow).mapNotNull { row -> + row.mapNotNull { builder -> + builder.buildButton(post) + }.takeIf { it.isNotEmpty() } + } + edit( + chatId, + messageId, + replyMarkup = InlineKeyboardMarkup(buttons) + ) + } + + onMessageDataCallbackQuery ( + initialFilter = { + it.data.startsWith(PanelButtonsAPI.openGlobalMenuDataPrefix) && it.message.chat.id == chatsConfig.sourceChatId + } + ) { + val postId = it.data.removePrefix(PanelButtonsAPI.openGlobalMenuDataPrefix).let(::PostId) + updatePost(postId, it.message.chat.id, it.message.messageId) + } + 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) + api.RootPanelButtonBuilder.buildButton(post) ?.let(::add) + } + ) + + val pushedButton = waitMessageDataCallbackQuery().first { + it.message.sameMessage(query.message) + } + + if (pushedButton.data == approveData) { + postsRepo.deleteById(postId) + } + } + + api.forceRefreshFlow.subscribeSafelyWithoutExceptions(this) { + val (chatId, messageId) = postsMessages.get(it) ?: return@subscribeSafelyWithoutExceptions + updatePost(it, chatId, messageId) + } + } +} 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/posts/panel/src/main/AndroidManifest.xml b/posts/panel/src/main/AndroidManifest.xml new file mode 100644 index 0000000..92f27c6 --- /dev/null +++ b/posts/panel/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/posts_registrar/src/jvmMain/kotlin/Plugin.kt b/posts_registrar/src/jvmMain/kotlin/Plugin.kt index 65eecd3..fd950b8 100644 --- a/posts_registrar/src/jvmMain/kotlin/Plugin.kt +++ b/posts_registrar/src/jvmMain/kotlin/Plugin.kt @@ -103,11 +103,6 @@ object Plugin : Plugin { NewPost( state.messages ) - ).firstOrNull() ?.let { - send(state.context, "Ok, you have registered ${it.content.size} messages as new post") - } ?: send( - state.context, - "Sorry, for some reason I was unable to register your post" ) null } diff --git a/ratings/source/build.gradle b/ratings/source/build.gradle index ea7c956..2c08fa6 100644 --- a/ratings/source/build.gradle +++ b/ratings/source/build.gradle @@ -12,6 +12,7 @@ kotlin { dependencies { api project(":plaguposter.common") api project(":plaguposter.ratings") + api project(":plaguposter.posts.panel") } } jvmMain { diff --git a/ratings/source/src/jvmMain/kotlin/Plugin.kt b/ratings/source/src/jvmMain/kotlin/Plugin.kt index a006328..4592508 100644 --- a/ratings/source/src/jvmMain/kotlin/Plugin.kt +++ b/ratings/source/src/jvmMain/kotlin/Plugin.kt @@ -1,5 +1,6 @@ package dev.inmo.plaguposter.ratings.source +import com.benasher44.uuid.uuid4 import dev.inmo.kslog.common.e import dev.inmo.kslog.common.logger import dev.inmo.micro_utils.coroutines.runCatchingSafely @@ -14,18 +15,28 @@ import dev.inmo.plaguposter.inlines.models.Format import dev.inmo.plaguposter.inlines.models.OfferTemplate import dev.inmo.plaguposter.inlines.repos.InlineTemplatesRepo import dev.inmo.plaguposter.posts.models.PostId +import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder +import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.ratings.models.Rating import dev.inmo.plaguposter.ratings.repo.RatingsRepo import dev.inmo.plaguposter.ratings.source.models.* import dev.inmo.plaguposter.ratings.source.repos.* +import dev.inmo.tgbotapi.extensions.api.answers.answer import dev.inmo.tgbotapi.extensions.api.delete import dev.inmo.tgbotapi.extensions.api.edit.edit import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.send import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.* +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.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton import dev.inmo.tgbotapi.types.message.textsources.regular +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.serialization.Serializable import kotlinx.serialization.json.* import org.jetbrains.exposed.sql.Database @@ -41,7 +52,8 @@ object Plugin : Plugin { @Serializable(RatingsVariantsSerializer::class) val variants: RatingsVariants, val autoAttach: Boolean, - val ratingOfferText: String + val ratingOfferText: String, + val panelButtonText: String = "Ratings" ) override fun Module.setupDI(database: Database, params: JsonObject) { @@ -66,6 +78,7 @@ object Plugin : Plugin { val ratingsRepo = koin.get() val postsRepo = koin.get() val config = koin.get() + val panelApi = koin.getOrNull() onPollUpdates (markerFactory = { it.id }) { poll -> val postId = pollsToPostsIdsRepo.get(poll.id) ?: return@onPollUpdates @@ -92,6 +105,7 @@ object Plugin : Plugin { pollsToPostsIdsRepo.set(sent.content.poll.id, postId) pollsToMessageInfoRepo.set(sent.content.poll.id, sent.short()) }.getOrNull() ?: continue + panelApi ?.forceRefresh(postId) return true } return false @@ -105,6 +119,8 @@ object Plugin : Plugin { delete(messageInfo.chatId, messageInfo.messageId) }.onFailure { this@Plugin.logger.e(it) { "Something went wrong when trying to remove ratings message ($messageInfo) for post $postId" } + }.onSuccess { + panelApi ?.forceRefresh(postId) }.isSuccess }.any().also { if (it) { @@ -209,5 +225,55 @@ object Plugin : Plugin { ) ) } + + panelApi ?.apply { + add( + PanelButtonBuilder { + CallbackDataInlineKeyboardButton( + config.panelButtonText + if (pollsToPostsIdsRepo.keys(it.id, firstPageWithOneElementPagination).results.any()) { + SuccessfulSymbol + } else { + UnsuccessfulSymbol + }, + "toggle_ratings ${it.id.string}" + ) + } + ) + onMessageDataCallbackQuery( + initialFilter = { + it.data.startsWith("toggle_ratings ") + } + ) { + val postId = it.data.removePrefix("toggle_ratings ").let(::PostId) + val post = postsRepo.getById(postId) ?: return@onMessageDataCallbackQuery + + val ratingPollAttached = pollsToPostsIdsRepo.keys(postId, firstPageWithOneElementPagination).results.any() + val toggleData = uuid4().toString() + + val editedMessage = edit( + it.message, + replyMarkup = flatInlineKeyboard { + dataButton( + if (ratingPollAttached) { + UnsuccessfulSymbol + } else { + SuccessfulSymbol + }, + toggleData + ) + panelApi.RootPanelButtonBuilder.buildButton(post) ?.let(::add) + } + ) + + val pushedButton = waitMessageDataCallbackQuery().first { it.message.sameMessage(editedMessage) } + if (pushedButton.data == toggleData) { + if (pollsToPostsIdsRepo.keys(postId, firstPageWithOneElementPagination).results.any()) { + detachPoll(postId) + } else { + attachPoll(postId) + } + } + } + } } } 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") diff --git a/runner/deploy.sh b/runner/deploy.sh index d5ed348..c99bb32 100755 --- a/runner/deploy.sh +++ b/runner/deploy.sh @@ -14,7 +14,7 @@ function assert_success() { } app=plaguposter -version=0.0.1 +version="`grep ../gradle.properties -e "^version=" | grep -e "[0-9.]*" -o`" server=docker.io/insanusmokrassar assert_success ../gradlew build diff --git a/settings.gradle b/settings.gradle index b7a91b9..4cfbdc9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,6 +3,7 @@ rootProject.name = 'plaguposter' String[] includes = [ ":common", ":posts", + ":posts:panel", ":posts_registrar", ":ratings", ":ratings:source", diff --git a/triggers/command/build.gradle b/triggers/command/build.gradle index 4f8449e..3f51e34 100644 --- a/triggers/command/build.gradle +++ b/triggers/command/build.gradle @@ -13,6 +13,7 @@ kotlin { api project(":plaguposter.common") api project(":plaguposter.posts") api project(":plaguposter.ratings.selector") + api project(":plaguposter.posts.panel") } } jvmMain { diff --git a/triggers/command/src/jvmMain/kotlin/Plugin.kt b/triggers/command/src/jvmMain/kotlin/Plugin.kt index 539cd5c..5423728 100644 --- a/triggers/command/src/jvmMain/kotlin/Plugin.kt +++ b/triggers/command/src/jvmMain/kotlin/Plugin.kt @@ -1,28 +1,42 @@ package dev.inmo.plaguposter.triggers.command +import com.benasher44.uuid.uuid4 import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.fsm.common.State +import dev.inmo.micro_utils.pagination.firstPageWithOneElementPagination import dev.inmo.plagubot.Plugin import dev.inmo.plaguposter.common.SuccessfulSymbol +import dev.inmo.plaguposter.common.UnsuccessfulSymbol import dev.inmo.plaguposter.inlines.models.Format import dev.inmo.plaguposter.inlines.models.OfferTemplate import dev.inmo.plaguposter.inlines.repos.InlineTemplatesRepo +import dev.inmo.plaguposter.posts.models.PostId +import dev.inmo.plaguposter.posts.panel.PanelButtonBuilder +import dev.inmo.plaguposter.posts.panel.PanelButtonsAPI import dev.inmo.plaguposter.posts.repo.PostsRepo import dev.inmo.plaguposter.posts.sending.PostPublisher import dev.inmo.plaguposter.ratings.selector.Selector +import dev.inmo.tgbotapi.extensions.api.answers.answer import dev.inmo.tgbotapi.extensions.api.edit.edit import dev.inmo.tgbotapi.extensions.api.send.reply import dev.inmo.tgbotapi.extensions.api.send.send import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContextWithFSM +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitTextMessage import dev.inmo.tgbotapi.extensions.behaviour_builder.strictlyOn import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand -import dev.inmo.tgbotapi.extensions.utils.botCommandTextSourceOrNull -import dev.inmo.tgbotapi.extensions.utils.contentMessageOrNull +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMessageDataCallbackQuery +import dev.inmo.tgbotapi.extensions.utils.* +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.ChatId import dev.inmo.tgbotapi.types.MessageIdentifier +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup import dev.inmo.tgbotapi.types.message.textsources.regular import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.serialization.Serializable import kotlinx.serialization.json.* import org.jetbrains.exposed.sql.Database @@ -36,13 +50,22 @@ object Plugin : Plugin { val sourceMessageId: MessageIdentifier, val messageInReply: MessageIdentifier ) : State + @Serializable + internal data class Config( + val panelButtonText: String? = "Publish" + ) override fun Module.setupDI(database: Database, params: JsonObject) { + params["publish_command"] ?.let { configJson -> + single { get().decodeFromJsonElement(Config.serializer(), configJson) } + } } override suspend fun BehaviourContextWithFSM.setupBotPlugin(koin: Koin) { val postsRepo = koin.get() val publisher = koin.get() val selector = koin.getOrNull() + val config = koin.getOrNull() + val panelApi = koin.getOrNull() onCommand("publish_post") { val messageInReply = it.replyTo ?.contentMessageOrNull() ?: run { @@ -87,5 +110,44 @@ object Plugin : Plugin { ) ) } + + panelApi ?.apply { + config ?.panelButtonText ?.let { text -> + add( + PanelButtonBuilder { + CallbackDataInlineKeyboardButton( + text, + "publish ${it.id.string}" + ) + } + ) + onMessageDataCallbackQuery( + initialFilter = { + it.data.startsWith("publish ") + } + ) { + val postId = it.data.removePrefix("publish ").let(::PostId) + val post = postsRepo.getById(postId) ?: return@onMessageDataCallbackQuery + + val publishData = uuid4().toString() + + val edited = edit( + it.message, + replyMarkup = flatInlineKeyboard { + dataButton(SuccessfulSymbol, publishData) + RootPanelButtonBuilder.buildButton(post) ?.let(::add) + } + ) + + val pushedButton = waitMessageDataCallbackQuery().first { + it.message.sameMessage(edited) + } + + if (pushedButton.data == publishData) { + publisher.publish(postId) + } + } + } + } } }