From 506f319c886a0e0e0967d385cfe6b5b5649bcd22 Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Thu, 25 Mar 2021 06:53:42 +0600 Subject: [PATCH] experimentally add CaptchaProvider --- build.gradle | 8 + gradle.properties | 2 +- .../inmo/plagubot/plugins/captcha/Plugin.kt | 73 +------ .../captcha/db/CaptchaChatsSettingsRepo.kt | 22 +- .../captcha/provider/CaptchaProvider.kt | 194 ++++++++++++++++++ .../plugins/captcha/settings/ChatSettings.kt | 13 +- 6 files changed, 221 insertions(+), 91 deletions(-) create mode 100644 src/main/kotlin/dev/inmo/plagubot/plugins/captcha/provider/CaptchaProvider.kt diff --git a/build.gradle b/build.gradle index 9a3afa7..0cc3c89 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,14 @@ repositories { password = project.hasProperty("GITHUB_TOKEN") ? project.getProperty("GITHUB_TOKEN") : System.getenv("GITHUB_TOKEN") } } + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/InsanusMokrassar/MicroUtils") + credentials { + username = project.hasProperty("GITHUB_USER") ? project.getProperty("GITHUB_USER") : System.getenv("GITHUB_USER") + password = project.hasProperty("GITHUB_TOKEN") ? project.getProperty("GITHUB_TOKEN") : System.getenv("GITHUB_TOKEN") + } + } } } diff --git a/gradle.properties b/gradle.properties index 138d54f..8ea0f0d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ kotlin_coroutines_version=1.4.3 kotlin_serialisation_runtime_version=1.1.0 plagubot_version=0.1.5 -micro_utils_version=0.4.30 +micro_utils_version=0.4.31-branch_0.4.31-build13 tgbotapi_libraries_version=0.0.2-branch_master-build12 project_group=dev.inmo diff --git a/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/Plugin.kt b/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/Plugin.kt index e4e6f91..aa855af 100644 --- a/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/Plugin.kt +++ b/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/Plugin.kt @@ -72,8 +72,7 @@ class CaptchaBotPlugin : Plugin { includeFilterByChatInBehaviourSubContext = false ) { safelyWithoutExceptions { deleteMessage(it) } - val eventDateTime = it.date - val chat = it.chat.requirePublicChat() + val chat = it.chat.requireGroupChat() val newUsers = it.chatEvent.members newUsers.forEach { user -> restrictChatMember( @@ -82,74 +81,8 @@ class CaptchaBotPlugin : Plugin { permissions = ChatPermissions() ) } - val settings = it.chat.settings() ?: return@onNewChatMembers - val userBanDateTime = eventDateTime + settings.checkTimeSpan - val authorized = Channel(newUsers.size) - val messagesToDelete = Channel(Channel.UNLIMITED) - val subContexts = newUsers.map { - doInSubContext(stopOnCompletion = false) { - val sentMessage = sendTextMessage( - chat, - buildEntities { - +it.mention(it.firstName) - regular(", ${settings.captchaText}") - } - ).also { messagesToDelete.send(it) } - val sentDice = sendDice( - sentMessage.chat, - SlotMachineDiceAnimationType, - replyToMessageId = sentMessage.messageId, - replyMarkup = slotMachineReplyMarkup() - ).also { messagesToDelete.send(it) } - val reels = sentDice.content.dice.calculateSlotMachineResult()!! - val leftToClick = mutableListOf( - reels.left.asSlotMachineReelImage.text, - reels.center.asSlotMachineReelImage.text, - reels.right.asSlotMachineReelImage.text - ) - - launch { - val clicked = arrayOf(null, null, null) - while (leftToClick.isNotEmpty()) { - val userClicked = waitDataCallbackQuery { if (user.id == it.id) this else null }.first() - if (userClicked.data == leftToClick.first()) { - clicked[3 - leftToClick.size] = leftToClick.removeAt(0) - if (clicked.contains(null)) { - safelyWithoutExceptions { answerCallbackQuery(userClicked, "Ok, next one") } - editMessageReplyMarkup(sentDice, slotMachineReplyMarkup(clicked[0], clicked[1], clicked[2])) - } else { - safelyWithoutExceptions { answerCallbackQuery(userClicked, "Thank you and welcome", showAlert = true) } - safelyWithoutExceptions { deleteMessage(sentMessage) } - safelyWithoutExceptions { deleteMessage(sentDice) } - } - } else { - safelyWithoutExceptions { answerCallbackQuery(userClicked, "Nope") } - } - } - authorized.send(it) - safelyWithoutExceptions { restrictChatMember(chat, it, permissions = LeftRestrictionsChatPermissions) } - stop() - } - - this to it - } - } - - delay((userBanDateTime - eventDateTime).millisecondsLong) - - authorized.close() - val authorizedUsers = authorized.toList() - - subContexts.forEach { (context, user) -> - if (user !in authorizedUsers) { - context.stop() - safelyWithoutExceptions { kickChatMember(chat, user) } - } - } - messagesToDelete.close() - for (message in messagesToDelete) { - executeUnsafe(DeleteMessage(message.chat.id, message.messageId), retries = 0) - } + val settings = it.chat.settings() + settings.captchaProvider.apply { doAction(it.date, chat, newUsers) } } if (adminsAPI != null) { diff --git a/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/db/CaptchaChatsSettingsRepo.kt b/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/db/CaptchaChatsSettingsRepo.kt index 6985139..6ce9875 100644 --- a/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/db/CaptchaChatsSettingsRepo.kt +++ b/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/db/CaptchaChatsSettingsRepo.kt @@ -1,22 +1,26 @@ package dev.inmo.plagubot.plugins.captcha.db import dev.inmo.micro_utils.repos.exposed.* -import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo +import dev.inmo.plagubot.plugins.captcha.provider.CaptchaProvider import dev.inmo.plagubot.plugins.captcha.settings.* import dev.inmo.tgbotapi.types.ChatId import dev.inmo.tgbotapi.types.toChatId +import kotlinx.serialization.json.Json import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.InsertStatement import org.jetbrains.exposed.sql.statements.UpdateStatement +private val captchaProviderSerialFormat = Json { + ignoreUnknownKeys = true +} + class CaptchaChatsSettingsRepo( override val database: Database ) : AbstractExposedCRUDRepo( tableName = "CaptchaChatsSettingsRepo" ) { private val chatIdColumn = long("chatId") - private val checkTimeSecondsColumn = integer("checkTime") - private val solveCaptchaTextColumn = text("solveCaptchaText") + private val captchaProviderColumn = text("captchaProvider") private val autoRemoveCommandsColumn = bool("autoRemoveCommands") override val primaryKey = PrimaryKey(chatIdColumn) @@ -29,23 +33,20 @@ class CaptchaChatsSettingsRepo( override fun insert(value: ChatSettings, it: InsertStatement) { it[chatIdColumn] = value.chatId.chatId - it[checkTimeSecondsColumn] = value.checkTime - it[solveCaptchaTextColumn] = value.captchaText + it[captchaProviderColumn] = captchaProviderSerialFormat.encodeToString(CaptchaProvider.serializer(), value.captchaProvider) it[autoRemoveCommandsColumn] = value.autoRemoveCommands } override fun update(id: ChatId, value: ChatSettings, it: UpdateStatement) { if (id.chatId == value.chatId.chatId) { - it[checkTimeSecondsColumn] = value.checkTime - it[solveCaptchaTextColumn] = value.captchaText + it[captchaProviderColumn] = captchaProviderSerialFormat.encodeToString(CaptchaProvider.serializer(), value.captchaProvider) it[autoRemoveCommandsColumn] = value.autoRemoveCommands } } override fun InsertStatement.asObject(value: ChatSettings): ChatSettings = ChatSettings( get(chatIdColumn).toChatId(), - get(checkTimeSecondsColumn), - get(solveCaptchaTextColumn), + captchaProviderSerialFormat.decodeFromString(CaptchaProvider.serializer(), get(captchaProviderColumn)), get(autoRemoveCommandsColumn) ) @@ -53,8 +54,7 @@ class CaptchaChatsSettingsRepo( override val ResultRow.asObject: ChatSettings get() = ChatSettings( get(chatIdColumn).toChatId(), - get(checkTimeSecondsColumn), - get(solveCaptchaTextColumn), + captchaProviderSerialFormat.decodeFromString(CaptchaProvider.serializer(), get(captchaProviderColumn)), get(autoRemoveCommandsColumn) ) diff --git a/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/provider/CaptchaProvider.kt b/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/provider/CaptchaProvider.kt new file mode 100644 index 0000000..8220566 --- /dev/null +++ b/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/provider/CaptchaProvider.kt @@ -0,0 +1,194 @@ +package dev.inmo.plagubot.plugins.captcha.provider + +import com.benasher44.uuid.uuid4 +import com.soywiz.klock.DateTime +import com.soywiz.klock.seconds +import dev.inmo.micro_utils.coroutines.safelyWithoutExceptions +import dev.inmo.plagubot.plugins.captcha.slotMachineReplyMarkup +import dev.inmo.tgbotapi.extensions.api.answers.answerCallbackQuery +import dev.inmo.tgbotapi.extensions.api.chat.members.kickChatMember +import dev.inmo.tgbotapi.extensions.api.chat.members.restrictChatMember +import dev.inmo.tgbotapi.extensions.api.deleteMessage +import dev.inmo.tgbotapi.extensions.api.edit.ReplyMarkup.editMessageReplyMarkup +import dev.inmo.tgbotapi.extensions.api.send.sendDice +import dev.inmo.tgbotapi.extensions.api.send.sendTextMessage +import dev.inmo.tgbotapi.extensions.behaviour_builder.* +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitDataCallbackQuery +import dev.inmo.tgbotapi.extensions.utils.asSlotMachineReelImage +import dev.inmo.tgbotapi.extensions.utils.calculateSlotMachineResult +import dev.inmo.tgbotapi.extensions.utils.formatting.buildEntities +import dev.inmo.tgbotapi.extensions.utils.formatting.regular +import dev.inmo.tgbotapi.extensions.utils.shortcuts.executeUnsafe +import dev.inmo.tgbotapi.extensions.utils.types.buttons.InlineKeyboardMarkup +import dev.inmo.tgbotapi.requests.DeleteMessage +import dev.inmo.tgbotapi.types.MessageEntity.textsources.mention +import dev.inmo.tgbotapi.types.Seconds +import dev.inmo.tgbotapi.types.User +import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton +import dev.inmo.tgbotapi.types.chat.LeftRestrictionsChatPermissions +import dev.inmo.tgbotapi.types.chat.abstracts.GroupChat +import dev.inmo.tgbotapi.types.dice.SlotMachineDiceAnimationType +import dev.inmo.tgbotapi.types.message.abstracts.Message +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.toList +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient + +@Serializable +sealed class CaptchaProvider { + abstract suspend fun BehaviourContext.doAction( + eventDateTime: DateTime, + chat: GroupChat, + newUsers: List + ) +} + +@Serializable +data class SlotMachineCaptchaProvider( + val checkTimeSeconds: Seconds = 300, + val captchaText: String = "solve this captcha: " +) : CaptchaProvider() { + @Transient + private val checkTimeSpan = checkTimeSeconds.seconds + + override suspend fun BehaviourContext.doAction( + eventDateTime: DateTime, + chat: GroupChat, + newUsers: List + ) { + val userBanDateTime = eventDateTime + checkTimeSpan + val authorized = Channel(newUsers.size) + val messagesToDelete = Channel(Channel.UNLIMITED) + val subContexts = newUsers.map { + doInSubContext(stopOnCompletion = false) { + val sentMessage = sendTextMessage( + chat, + buildEntities { + +it.mention(it.firstName) + regular(", ${captchaText}") + } + ).also { messagesToDelete.send(it) } + val sentDice = sendDice( + sentMessage.chat, + SlotMachineDiceAnimationType, + replyToMessageId = sentMessage.messageId, + replyMarkup = slotMachineReplyMarkup() + ).also { messagesToDelete.send(it) } + val reels = sentDice.content.dice.calculateSlotMachineResult()!! + val leftToClick = mutableListOf( + reels.left.asSlotMachineReelImage.text, + reels.center.asSlotMachineReelImage.text, + reels.right.asSlotMachineReelImage.text + ) + + launch { + val clicked = arrayOf(null, null, null) + while (leftToClick.isNotEmpty()) { + val userClicked = waitDataCallbackQuery { if (user.id == it.id) this else null }.first() + if (userClicked.data == leftToClick.first()) { + clicked[3 - leftToClick.size] = leftToClick.removeAt(0) + if (clicked.contains(null)) { + safelyWithoutExceptions { answerCallbackQuery(userClicked, "Ok, next one") } + editMessageReplyMarkup(sentDice, slotMachineReplyMarkup(clicked[0], clicked[1], clicked[2])) + } else { + safelyWithoutExceptions { answerCallbackQuery(userClicked, "Thank you and welcome", showAlert = true) } + safelyWithoutExceptions { deleteMessage(sentMessage) } + safelyWithoutExceptions { deleteMessage(sentDice) } + } + } else { + safelyWithoutExceptions { answerCallbackQuery(userClicked, "Nope") } + } + } + authorized.send(it) + safelyWithoutExceptions { restrictChatMember(chat, it, permissions = LeftRestrictionsChatPermissions) } + stop() + } + + this to it + } + } + + delay((userBanDateTime - eventDateTime).millisecondsLong) + + authorized.close() + val authorizedUsers = authorized.toList() + + subContexts.forEach { (context, user) -> + if (user !in authorizedUsers) { + context.stop() + safelyWithoutExceptions { kickChatMember(chat, user) } + } + } + messagesToDelete.close() + for (message in messagesToDelete) { + executeUnsafe(DeleteMessage(message.chat.id, message.messageId), retries = 0) + } + } +} + +@Serializable +data class SimpleCaptchaProvider( + val checkTimeSeconds: Seconds = 60, + val captchaText: String = "press this button to pass captcha:", + val buttonText: String = "Press me\uD83D\uDE0A", + val kick: Boolean = true +) : CaptchaProvider() { + @Transient + private val checkTimeSpan = checkTimeSeconds.seconds + + override suspend fun BehaviourContext.doAction( + eventDateTime: DateTime, + chat: GroupChat, + newUsers: List + ) { + val userBanDateTime = eventDateTime + checkTimeSpan + newUsers.mapNotNull { + safelyWithoutExceptions { + launch { + doInSubContext { + val callbackData = uuid4().toString() + val sentMessage = sendTextMessage( + chat, + buildEntities { + +it.mention(it.firstName) + regular(", $captchaText") + }, + replyMarkup = InlineKeyboardMarkup( + CallbackDataInlineKeyboardButton(buttonText, callbackData) + ) + ) + + suspend fun removeRedundantMessages() { + safelyWithoutExceptions { + deleteMessage(sentMessage) + } + } + + val job = launch { + waitDataCallbackQuery { + if (it.id == user.id && this.data == callbackData) { + this + } else { + null + } + }.first() + + removeRedundantMessages() + safelyWithoutExceptions { restrictChatMember(chat, it, permissions = LeftRestrictionsChatPermissions) } + stop() + } + + delay((userBanDateTime - eventDateTime).millisecondsLong) + + job.cancel() + if (kick) { + safelyWithoutExceptions { kickChatMember(chat, it) } + } + } + } + } + }.joinAll() + } +} + diff --git a/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/settings/ChatSettings.kt b/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/settings/ChatSettings.kt index a46ac09..4548b0a 100644 --- a/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/settings/ChatSettings.kt +++ b/src/main/kotlin/dev/inmo/plagubot/plugins/captcha/settings/ChatSettings.kt @@ -1,18 +1,13 @@ package dev.inmo.plagubot.plugins.captcha.settings -import com.soywiz.klock.TimeSpan +import dev.inmo.plagubot.plugins.captcha.provider.CaptchaProvider +import dev.inmo.plagubot.plugins.captcha.provider.SimpleCaptchaProvider import dev.inmo.tgbotapi.types.ChatId -import dev.inmo.tgbotapi.types.Seconds import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient @Serializable data class ChatSettings( val chatId: ChatId, - val checkTime: Seconds = 60, - val captchaText: String = "solve next captcha:", + val captchaProvider: CaptchaProvider = SimpleCaptchaProvider(), val autoRemoveCommands: Boolean = false -) { - @Transient - val checkTimeSpan = TimeSpan(checkTime * 1000.0) -} +)