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..3b62b06 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,14 +1,16 @@ [versions] kotlin = "1.6.21" -plagubot = "1.2.2" +plagubot = "1.2.3" kslog = "0.3.2" +plagubot-commands = "0.1.0" [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" } kslog = { module = "dev.inmo:kslog", version.ref = "kslog" } # Libs for classpath 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..ddb46de 100644 --- a/welcome/build.gradle +++ b/welcome/build.gradle @@ -23,4 +23,5 @@ dependencies { implementation libs.kotlin api libs.plagubot.plugin api libs.kslog + api libs.plagubot.commands } diff --git a/welcome/src/main/kotlin/UserAdminChecker.kt b/welcome/src/main/kotlin/UserAdminChecker.kt new file mode 100644 index 0000000..9217c19 --- /dev/null +++ b/welcome/src/main/kotlin/UserAdminChecker.kt @@ -0,0 +1,11 @@ +import dev.inmo.tgbotapi.bot.TelegramBot +import dev.inmo.tgbotapi.extensions.api.chat.get.getChatAdministrators +import dev.inmo.tgbotapi.types.chat.GroupChat +import dev.inmo.tgbotapi.types.chat.User + +suspend fun TelegramBot.userIsAdmin(user: User, chat: GroupChat): Boolean { + val chatAdmins = getChatAdministrators(chat) + val chatAdminsIds = chatAdmins.map { adminMember -> adminMember.user.id } + + return user.id in chatAdminsIds +} diff --git a/welcome/src/main/kotlin/WelcomePlugin.kt b/welcome/src/main/kotlin/WelcomePlugin.kt index 5998a2a..c795240 100644 --- a/welcome/src/main/kotlin/WelcomePlugin.kt +++ b/welcome/src/main/kotlin/WelcomePlugin.kt @@ -1,17 +1,40 @@ +import db.WelcomeTable import dev.inmo.kslog.common.logger import dev.inmo.kslog.common.w +import dev.inmo.micro_utils.coroutines.runCatchingSafely +import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.plagubot.Plugin -import dev.inmo.tgbotapi.extensions.api.send.reply -import dev.inmo.tgbotapi.extensions.api.send.sendMessage +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.chat.get.getChatAdministrators +import dev.inmo.tgbotapi.extensions.api.edit.edit +import dev.inmo.tgbotapi.extensions.api.send.* import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext -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.expectations.waitContentMessage +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery +import dev.inmo.tgbotapi.extensions.behaviour_builder.oneOf +import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.* +import dev.inmo.tgbotapi.extensions.utils.* +import dev.inmo.tgbotapi.extensions.utils.formatting.* +import dev.inmo.tgbotapi.extensions.utils.types.buttons.* +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.types.message.content.TextContent +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.serialization.Serializable import kotlinx.serialization.json.* +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,27 +53,182 @@ 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 = 60L ) /** * 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( + welcomeTable: WelcomeTable, + config: Config, + groupMessage: CommonGroupContentMessage + ) { + val user = groupMessage.user + + if (userIsAdmin(user, groupMessage.chat)) { + val cancelData = "cancel_${groupMessage.chat.id}" + val unsetData = "unset_${groupMessage.chat.id}" + + val sentMessage = sendMessage( + user, + buildEntities { + regular("Ok, send me the message which should be used as welcome message for chat ") + underline(groupMessage.chat.title) + }, + replyMarkup = inlineKeyboard { + row { + dataButton("Unset", unsetData) + dataButton("Cancel", cancelData) + } + } + ) + + oneOf( + async { + val query = waitMessageDataCallbackQuery().filter { + it.data == unsetData + && it.message.chat.id == sentMessage.chat.id + && it.message.messageId == sentMessage.messageId + }.first() + + if (welcomeTable.unset(groupMessage.chat.id)) { + edit( + sentMessage, + buildEntities { + regular("Welcome message has been removed for chat ") + underline(groupMessage.chat.title) + } + ) + } else { + edit( + sentMessage, + buildEntities { + regular("Something went wrong on welcome message unsetting for chat ") + underline(groupMessage.chat.title) + } + ) + } + + answer(query) + }, + async { + val query = waitMessageDataCallbackQuery().filter { + it.data == cancelData + && it.message.chat.id == sentMessage.chat.id + && it.message.messageId == sentMessage.messageId + }.first() + + edit( + sentMessage, + buildEntities { + regular("You have cancelled change of welcome message for chat ") + underline(groupMessage.chat.title) + } + ) + + answer(query) + }, + async { + val message = waitContentMessage().filter { + it.chat.id == sentMessage.chat.id + }.first() + + val success = welcomeTable.set( + ChatSettings( + groupMessage.chat.id, + message.chat.id, + message.messageId + ) + ) + + if (success) { + reply( + message, + buildEntities { + 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 { + reply( + message, + buildEntities { + regular("Something went wrong on welcome message changing for chat ") + underline(groupMessage.chat.title) + } + ) + } + }, + async { + while (isActive) { + delay(config.recheckOfAdmin) + + if (!userIsAdmin(user, groupMessage.chat)) { + edit(sentMessage, "Sorry, but you are not admin in chat ${groupMessage.chat.title} more") + 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() + + onCommand( + "welcome", + initialFilter = { + it.chat is GroupChat + } + ) { + it.whenCommonGroupContentMessage { groupMessage -> + launch { + handleWelcomeCommand(welcomeTable, config, groupMessage) + } + } + } + + onNewChatMembers { + val chatSettings = welcomeTable.get(it.chat.id) + + if (chatSettings == null) { + return@onNewChatMembers + } + + try { + copyMessage( + it.chat.id, + chatSettings.sourceChatId, + chatSettings.sourceMessageId + ) + } catch (e: RequestException) { + welcomeTable.unset(it.chat.id) + } + } + + + allUpdatesFlow.subscribeSafelyWithoutExceptions(scope) { + println(it) } } 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 +)