diff --git a/build.gradle b/build.gradle index 423b937..fd52fa2 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,7 @@ dependencies { implementation libs.plagubot implementation project(":introduction") + implementation project(":welcome") } application { diff --git a/config.json b/config.json index 6878d1e..483d3a6 100644 --- a/config.json +++ b/config.json @@ -1,12 +1,15 @@ { "botToken": "1234567890:ABCDEFGHIJKLMNOP_qrstuvwxyz12345678", "plugins": [ - "IntroductionPlugin" + "dev.inmo.plagubot.plugins.commands.CommandsPlugin", + + "IntroductionPlugin", + "WelcomePlugin" ], "introduction": { "onStartCommandMessage": "Hello World" }, "database": { - "url": "jdbc:sqlite:file:local.db?mode=memory&cache=shared" + "url": "jdbc:sqlite:file:local.db" } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5857ec4..4c986e5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,18 @@ [versions] -kotlin = "1.6.21" -plagubot = "1.2.2" -kslog = "0.3.2" +kotlin = "1.7.10" +plagubot = "2.3.3" +kslog = "0.5.2" +plagubot-commands = "0.3.4" +tgbotapi-libraries = "0.5.4" [libraries] kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } plagubot = { module = "dev.inmo:plagubot.bot", version.ref = "plagubot" } plagubot-plugin = { module = "dev.inmo:plagubot.plugin", version.ref = "plagubot" } +plagubot-commands = { module = "dev.inmo:plagubot.plugins.commands", version.ref = "plagubot-commands" } +tgbotapi-libraries-admins = { module = "dev.inmo:tgbotapi.libraries.cache.admins.plagubot", version.ref = "tgbotapi-libraries" } kslog = { module = "dev.inmo:kslog", version.ref = "kslog" } # Libs for classpath diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8049c68..ae04661 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index 8fd4ae8..ecd58c9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,3 +1,4 @@ rootProject.name = "tutorial" include ":introduction" +include ":welcome" diff --git a/welcome/build.gradle b/welcome/build.gradle index 3b4d2f0..e0b0ad7 100644 --- a/welcome/build.gradle +++ b/welcome/build.gradle @@ -23,4 +23,6 @@ dependencies { implementation libs.kotlin api libs.plagubot.plugin api libs.kslog + api libs.plagubot.commands + api libs.tgbotapi.libraries.admins } diff --git a/welcome/src/main/kotlin/WelcomePlugin.kt b/welcome/src/main/kotlin/WelcomePlugin.kt index 5998a2a..b1dc48a 100644 --- a/welcome/src/main/kotlin/WelcomePlugin.kt +++ b/welcome/src/main/kotlin/WelcomePlugin.kt @@ -1,17 +1,43 @@ +import db.WelcomeTable +import dev.inmo.kslog.common.e import dev.inmo.kslog.common.logger -import dev.inmo.kslog.common.w import dev.inmo.plagubot.Plugin +import dev.inmo.plagubot.plugins.commands.full +import dev.inmo.tgbotapi.bot.exceptions.RequestException +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.sendMessage -import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext +import dev.inmo.tgbotapi.extensions.api.send.send +import dev.inmo.tgbotapi.extensions.behaviour_builder.* +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitContentMessage +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand -import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onMyChatMemberUpdated -import dev.inmo.tgbotapi.types.chat.PrivateChat +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onNewChatMembers +import dev.inmo.tgbotapi.extensions.utils.extensions.sameChat +import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage +import dev.inmo.tgbotapi.extensions.utils.ifCommonGroupContentMessage +import dev.inmo.tgbotapi.extensions.utils.types.buttons.dataButton +import dev.inmo.tgbotapi.extensions.utils.types.buttons.flatInlineKeyboard +import dev.inmo.tgbotapi.libraries.cache.admins.AdminsCacheAPI +import dev.inmo.tgbotapi.types.BotCommand +import dev.inmo.tgbotapi.types.MilliSeconds +import dev.inmo.tgbotapi.types.chat.GroupChat +import dev.inmo.tgbotapi.types.commands.BotCommandScope +import dev.inmo.tgbotapi.types.message.abstracts.CommonGroupContentMessage +import dev.inmo.tgbotapi.types.message.content.MessageContent +import dev.inmo.tgbotapi.utils.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.serialization.Serializable -import kotlinx.serialization.json.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import model.ChatSettings import org.jetbrains.exposed.sql.Database import org.koin.core.Koin import org.koin.core.module.Module +import org.koin.core.qualifier.named /** * This is template of plugin with preset [log]ger, [Config] and template configurations of [setupDI] and [setupBotPlugin]. @@ -30,31 +56,154 @@ class WelcomePlugin : Plugin { * See realization of [setupDI] to get know how this class will be deserialized from global config * * See realization of [setupBotPlugin] to get know how to get access to this class + * + * @param recheckOfAdmin This parameter will be used before setup of */ @Serializable private class Config( + val recheckOfAdmin: MilliSeconds = 60000L ) /** * DI configuration of current plugin. Here we are decoding [Config] and put it into [Module] receiver */ override fun Module.setupDI(database: Database, params: JsonObject) { - single { get().decodeFromJsonElement(Config.serializer(), params[pluginConfigSectionName] ?: return@single null) } + single { get().decodeFromJsonElement(Config.serializer(), params[pluginConfigSectionName] ?: return@single Config()) } + single { WelcomeTable(database) } + single(named("welcome")) { BotCommand("welcome", "Use to setup welcome message").full(BotCommandScope.AllChatAdministrators) } + } + + private suspend fun BehaviourContext.handleWelcomeCommand( + adminsCacheAPI: AdminsCacheAPI, + welcomeTable: WelcomeTable, + config: Config, + groupMessage: CommonGroupContentMessage + ) { + val user = groupMessage.user + + if (adminsCacheAPI.isAdmin(groupMessage.chat.id, user.id)) { + val previousMessage = welcomeTable.get(groupMessage.chat.id) + val sentMessage = send( + user, + replyMarkup = flatInlineKeyboard { + if (previousMessage != null) { + dataButton("Unset", unsetData) + } + dataButton("Cancel", cancelData) + } + ) { + regular("Ok, send me the message which should be used as welcome message for chat ") + underline(groupMessage.chat.title) + } + + oneOf( + parallel { + val query = waitMessageDataCallbackQuery().filter { + it.data == unsetData && it.message.sameMessage(sentMessage) + }.first() + + edit(sentMessage) { + if (welcomeTable.unset(groupMessage.chat.id)) { + regular("Welcome message has been removed for chat ") + underline(groupMessage.chat.title) + } else { + regular("Something went wrong on welcome message unsetting for chat ") + underline(groupMessage.chat.title) + } + } + + answer(query) + }, + parallel { + val query = waitMessageDataCallbackQuery().filter { + it.data == cancelData && it.message.sameMessage(sentMessage) + }.first() + + edit(sentMessage) { + regular("You have cancelled change of welcome message for chat ") + underline(groupMessage.chat.title) + } + + answer(query) + }, + parallel { + val message = waitContentMessage().filter { + it.sameChat(sentMessage) + }.first() + + val success = welcomeTable.set( + ChatSettings( + groupMessage.chat.id, + message.chat.id, + message.messageId + ) + ) + + reply(message) { + if (success) { + regular("Welcome message has been changed for chat ") + underline(groupMessage.chat.title) + regular(".\n\n") + bold("Please, do not delete this message if you want it to work and don't stop this bot to keep welcome message works right") + } else { + regular("Something went wrong on welcome message changing for chat ") + underline(groupMessage.chat.title) + } + } + delete(sentMessage) + }, + parallel { + while (isActive) { + delay(config.recheckOfAdmin) + + if (adminsCacheAPI.isAdmin(groupMessage.chat.id, user.id)) { + edit(sentMessage, "Sorry, but you are not admin in chat ${groupMessage.chat.title} anymore") + break + } + } + } + ) + } } /** * Final configuration of bot. Here we are getting [Config] from [koin] */ override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { - val config = koin.getOrNull() + val config = koin.get() - if (config == null) { - log.w("Plugin has been disabled due to absence of \"$pluginConfigSectionName\" field in config or some error during configuration loading") - return + val welcomeTable = koin.get() + val adminsCacheAPI = koin.get() + + onCommand( + "welcome", + initialFilter = { it.chat is GroupChat } + ) { + it.ifCommonGroupContentMessage { groupMessage -> + launch { + handleWelcomeCommand(adminsCacheAPI, welcomeTable, config, groupMessage) + } + } + } + + onNewChatMembers { + val chatSettings = welcomeTable.get(it.chat.id) + + if (chatSettings == null) { + return@onNewChatMembers + } + + reply( + it, + chatSettings.sourceChatId, + chatSettings.sourceMessageId + ) } } companion object { private const val pluginConfigSectionName = "welcome" + private const val cancelData = "cancel" + private const val unsetData = "unset" } } diff --git a/welcome/src/main/kotlin/db/WelcomeTable.kt b/welcome/src/main/kotlin/db/WelcomeTable.kt new file mode 100644 index 0000000..62a3de1 --- /dev/null +++ b/welcome/src/main/kotlin/db/WelcomeTable.kt @@ -0,0 +1,44 @@ +package db + +import dev.inmo.micro_utils.repos.exposed.ExposedRepo +import dev.inmo.micro_utils.repos.exposed.initTable +import dev.inmo.tgbotapi.types.ChatId +import model.ChatSettings +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction + +internal class WelcomeTable( + override val database: Database +) : Table("welcome"), ExposedRepo { + val targetChatIdColumn = long("targetChatId").uniqueIndex() + val sourceChatIdColumn = long("sourceChatId") + val sourceMessageIdColumn = long("sourceMessageId") + override val primaryKey: PrimaryKey = PrimaryKey(targetChatIdColumn) + + init { + initTable() + } + + fun get(chatId: ChatId): ChatSettings? = transaction(database) { + select { targetChatIdColumn.eq(chatId.chatId) }.limit(1).firstOrNull() ?.let { + ChatSettings( + ChatId(it[targetChatIdColumn]), + ChatId(it[sourceChatIdColumn]), + it[sourceMessageIdColumn] + ) + } + } + + fun set(chatSettings: ChatSettings): Boolean = transaction(database) { + deleteWhere { targetChatIdColumn.eq(chatSettings.targetChatId.chatId) } + insert { + it[targetChatIdColumn] = chatSettings.targetChatId.chatId + it[sourceChatIdColumn] = chatSettings.sourceChatId.chatId + it[sourceMessageIdColumn] = chatSettings.sourceMessageId + }.insertedCount > 0 + } + + fun unset(chatId: ChatId): Boolean = transaction(database) { + deleteWhere { targetChatIdColumn.eq(chatId.chatId) } > 0 + } +} diff --git a/welcome/src/main/kotlin/model/ChatSettings.kt b/welcome/src/main/kotlin/model/ChatSettings.kt new file mode 100644 index 0000000..17e668f --- /dev/null +++ b/welcome/src/main/kotlin/model/ChatSettings.kt @@ -0,0 +1,12 @@ +package model + +import dev.inmo.tgbotapi.types.ChatId +import dev.inmo.tgbotapi.types.MessageIdentifier +import kotlinx.serialization.Serializable + +@Serializable +internal data class ChatSettings( + val targetChatId: ChatId, + val sourceChatId: ChatId, + val sourceMessageId: MessageIdentifier +)