mirror of
				https://github.com/InsanusMokrassar/CaptchaPlaguBotPlugin.git
				synced 2025-10-31 11:30:34 +00:00 
			
		
		
		
	Compare commits
	
		
			65 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9883c47e1b | |||
| a6d347934b | |||
| de5391b121 | |||
| 823c895e42 | |||
| de6a8241c3 | |||
| 7dead36cd9 | |||
| c84cff9ddc | |||
| 418d6b7f45 | |||
| 2165f665ec | |||
| 27dffce0d2 | |||
| 772f05729e | |||
| dae42f95b7 | |||
| 70097c731f | |||
| 5bced22b47 | |||
| 16831b520e | |||
| d8dbb2512f | |||
| f1093d6944 | |||
| 58c6d12bdd | |||
| b678b34cc8 | |||
| 94cbeacda6 | |||
| 3430595e8a | |||
| 250877459f | |||
| 8fff301ead | |||
| 7fb5e853b7 | |||
| cef6ce391b | |||
| 959b497280 | |||
| fb9933a1de | |||
| 012922cd0e | |||
| 39c7b42778 | |||
| e375170567 | |||
| 8017678da8 | |||
| 317db251b9 | |||
| 43cdef96d0 | |||
| 7d76814a41 | |||
| 4fbb752b8f | |||
| 306eccf380 | |||
| d375875067 | |||
| 0e097fc9ba | |||
| 852262853e | |||
| 88047703ad | |||
| 867a2b6fe5 | |||
| f4019f67e2 | |||
| 6235837cee | |||
| f952db018e | |||
| 5c7d9dce05 | |||
| 4a0e2cc843 | |||
| 5e52e2c32e | |||
| 98f07d6611 | |||
| 0cb1b45c0e | |||
| 9fa72f8716 | |||
| 6fe5f96e4e | |||
| 338e97770d | |||
| 1520a670c4 | |||
| 3a7ef56565 | |||
| 4a7339afd9 | |||
| 1d4baa8be9 | |||
| b3a9a9875f | |||
| 8cc2503934 | |||
| 87a6cab33e | |||
| f84edb7860 | |||
| 506f319c88 | |||
| b09bcd1d75 | |||
| 7afd83e052 | |||
| a53c0ceed8 | |||
| 8f25f8233e | 
							
								
								
									
										4
									
								
								.github/workflows/publish_package.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/publish_package.yml
									
									
									
									
										vendored
									
									
								
							| @@ -8,7 +8,7 @@ jobs: | |||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v2 | ||||||
|       - uses: actions/setup-java@v1 |       - uses: actions/setup-java@v1 | ||||||
|         with: |         with: | ||||||
|           java-version: 1.8 |           java-version: 11 | ||||||
|       - name: Update version |       - name: Update version | ||||||
|         run: | |         run: | | ||||||
|           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" |           branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`" | ||||||
| @@ -19,7 +19,7 @@ jobs: | |||||||
|           GITHUB_USER: ${{ github.actor }} |           GITHUB_USER: ${{ github.actor }} | ||||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||
|       - name: Publish package |       - name: Publish package | ||||||
|         run: ./gradlew publishAllPublicationsToGithubPackagesRepository --no-parallel -x signMavenPublication |         run: ./gradlew publishAllPublicationsToGithubPackagesRepository --no-parallel | ||||||
|         env: |         env: | ||||||
|           GITHUBPACKAGES_USER: ${{ github.actor }} |           GITHUBPACKAGES_USER: ${{ github.actor }} | ||||||
|           GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} |           GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }} | ||||||
|   | |||||||
| @@ -32,6 +32,14 @@ repositories { | |||||||
|                 password = project.hasProperty("GITHUB_TOKEN") ? project.getProperty("GITHUB_TOKEN") : System.getenv("GITHUB_TOKEN") |                 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") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -43,4 +51,5 @@ dependencies { | |||||||
|     api "dev.inmo:plagubot.plugin:$plagubot_version" |     api "dev.inmo:plagubot.plugin:$plagubot_version" | ||||||
|     api "dev.inmo:micro_utils.repos.exposed:$micro_utils_version" |     api "dev.inmo:micro_utils.repos.exposed:$micro_utils_version" | ||||||
|     api "dev.inmo:tgbotapi.libraries.cache.admins.plagubot:$tgbotapi_libraries_version" |     api "dev.inmo:tgbotapi.libraries.cache.admins.plagubot:$tgbotapi_libraries_version" | ||||||
|  |     api "dev.inmo:plagubot.plugins.commands:$commands_version" | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,13 +4,14 @@ org.gradle.parallel=true | |||||||
| kotlin.js.generate.externals=true | kotlin.js.generate.externals=true | ||||||
| kotlin.incremental=true | kotlin.incremental=true | ||||||
|  |  | ||||||
| kotlin_version=1.4.31 | kotlin_version=1.7.10 | ||||||
| kotlin_coroutines_version=1.4.3 | kotlin_coroutines_version=1.6.4 | ||||||
| kotlin_serialisation_runtime_version=1.1.0 | kotlin_serialisation_runtime_version=1.4.0 | ||||||
| plagubot_version=0.1.5 | plagubot_version=2.3.3 | ||||||
|  |  | ||||||
| micro_utils_version=0.4.30 | micro_utils_version=0.12.13 | ||||||
| tgbotapi_libraries_version=0.0.2-branch_master-build12 | tgbotapi_libraries_version=0.5.4 | ||||||
|  | commands_version=0.3.4 | ||||||
|  |  | ||||||
| project_group=dev.inmo | project_group=dev.inmo | ||||||
| project_version=0.1.4 | project_version=0.1.6 | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,5 @@ | |||||||
| distributionBase=GRADLE_USER_HOME | distributionBase=GRADLE_USER_HOME | ||||||
| distributionPath=wrapper/dists | distributionPath=wrapper/dists | ||||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip | ||||||
| zipStoreBase=GRADLE_USER_HOME | zipStoreBase=GRADLE_USER_HOME | ||||||
| zipStorePath=wrapper/dists | zipStorePath=wrapper/dists | ||||||
|   | |||||||
| @@ -1,6 +1,4 @@ | |||||||
| apply plugin: 'maven-publish' | apply plugin: 'maven-publish' | ||||||
| apply plugin: 'signing' |  | ||||||
|  |  | ||||||
|  |  | ||||||
| task javadocJar(type: Jar) { | task javadocJar(type: Jar) { | ||||||
|     from javadoc |     from javadoc | ||||||
| @@ -76,7 +74,18 @@ publishing { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | if (project.hasProperty("signing.gnupg.keyName")) { | ||||||
|  |     apply plugin: 'signing' | ||||||
|  |      | ||||||
|     signing { |     signing { | ||||||
|         useGpgCmd() |         useGpgCmd() | ||||||
|  |      | ||||||
|         sign publishing.publications |         sign publishing.publications | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     task signAll { | ||||||
|  |         tasks.withType(Sign).forEach { | ||||||
|  |             dependsOn(it) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| {"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/CaptchaPlaguBotPlugin/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"${project.name}","url":"https://github.com/InsanusMokrassar/CaptchaPlaguBotPlugin","vcsUrl":"https://github.com/InsanusMokrassar/CaptchaPlaguBotPlugin.git","includeGpgSigning":true,"developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/CaptchaPlaguBotPlugin"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}]},"type":"JVM"} | {"licenses":[{"id":"Apache-2.0","title":"Apache Software License 2.0","url":"https://github.com/InsanusMokrassar/CaptchaPlaguBotPlugin/blob/master/LICENSE"}],"mavenConfig":{"name":"${project.name}","description":"${project.name}","url":"https://github.com/InsanusMokrassar/CaptchaPlaguBotPlugin","vcsUrl":"https://github.com/InsanusMokrassar/CaptchaPlaguBotPlugin.git","developers":[{"id":"InsanusMokrassar","name":"Ovsiannikov Aleksei","eMail":"ovsyannikov.alexey95@gmail.com"}],"repositories":[{"name":"GithubPackages","url":"https://maven.pkg.github.com/InsanusMokrassar/CaptchaPlaguBotPlugin"},{"name":"sonatype","url":"https://oss.sonatype.org/service/local/staging/deploy/maven2/"}],"gpgSigning":{"type":"dev.inmo.kmppscriptbuilder.core.models.GpgSigning.Optional"}},"type":"JVM"} | ||||||
| @@ -1,158 +1,247 @@ | |||||||
| package dev.inmo.plagubot.plugins.captcha | package dev.inmo.plagubot.plugins.captcha | ||||||
|  |  | ||||||
|  | import com.benasher44.uuid.uuid4 | ||||||
| import dev.inmo.micro_utils.coroutines.* | import dev.inmo.micro_utils.coroutines.* | ||||||
| import dev.inmo.micro_utils.repos.create | import dev.inmo.micro_utils.repos.create | ||||||
| import dev.inmo.plagubot.Plugin | import dev.inmo.plagubot.Plugin | ||||||
|  | import dev.inmo.plagubot.plugins.captcha.cas.CASChecker | ||||||
|  | import dev.inmo.plagubot.plugins.captcha.cas.KtorCASChecker | ||||||
| import dev.inmo.plagubot.plugins.captcha.db.CaptchaChatsSettingsRepo | import dev.inmo.plagubot.plugins.captcha.db.CaptchaChatsSettingsRepo | ||||||
|  | import dev.inmo.plagubot.plugins.captcha.provider.* | ||||||
| import dev.inmo.plagubot.plugins.captcha.settings.ChatSettings | import dev.inmo.plagubot.plugins.captcha.settings.ChatSettings | ||||||
| import dev.inmo.tgbotapi.bot.TelegramBot | import dev.inmo.plagubot.plugins.commands.BotCommandFullInfo | ||||||
| import dev.inmo.tgbotapi.extensions.api.answers.answerCallbackQuery | import dev.inmo.plagubot.plugins.commands.CommandsKeeperKey | ||||||
| import dev.inmo.tgbotapi.extensions.api.chat.members.* | import dev.inmo.tgbotapi.extensions.api.chat.members.* | ||||||
|  | import dev.inmo.tgbotapi.extensions.api.delete | ||||||
| import dev.inmo.tgbotapi.extensions.api.deleteMessage | import dev.inmo.tgbotapi.extensions.api.deleteMessage | ||||||
| import dev.inmo.tgbotapi.extensions.api.edit.ReplyMarkup.editMessageReplyMarkup | import dev.inmo.tgbotapi.extensions.api.send.* | ||||||
| import dev.inmo.tgbotapi.extensions.api.send.media.reply |  | ||||||
| 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.* | ||||||
| import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitBaseInlineQuery |  | ||||||
| import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitDataCallbackQuery |  | ||||||
| import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand | import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onCommand | ||||||
| import dev.inmo.tgbotapi.updateshandlers.FlowsUpdatesFilter |  | ||||||
| import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onNewChatMembers | import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onNewChatMembers | ||||||
| import dev.inmo.tgbotapi.extensions.utils.* | import dev.inmo.tgbotapi.extensions.utils.* | ||||||
| import dev.inmo.tgbotapi.extensions.utils.formatting.buildEntities | import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithParams | ||||||
| import dev.inmo.tgbotapi.extensions.utils.formatting.regular |  | ||||||
| import dev.inmo.tgbotapi.extensions.utils.shortcuts.executeUnsafe |  | ||||||
| import dev.inmo.tgbotapi.libraries.cache.admins.* | import dev.inmo.tgbotapi.libraries.cache.admins.* | ||||||
| import dev.inmo.tgbotapi.requests.DeleteMessage |  | ||||||
| import dev.inmo.tgbotapi.types.BotCommand | import dev.inmo.tgbotapi.types.BotCommand | ||||||
| import dev.inmo.tgbotapi.types.MessageEntity.textsources.mention | import dev.inmo.tgbotapi.types.chat.* | ||||||
| import dev.inmo.tgbotapi.types.User | import dev.inmo.tgbotapi.types.commands.BotCommandScope | ||||||
| import dev.inmo.tgbotapi.types.chat.ChatPermissions | import dev.inmo.tgbotapi.utils.link | ||||||
| import dev.inmo.tgbotapi.types.chat.LeftRestrictionsChatPermissions | import dev.inmo.tgbotapi.utils.mention | ||||||
| import dev.inmo.tgbotapi.types.chat.abstracts.Chat | import io.ktor.client.HttpClient | ||||||
| import dev.inmo.tgbotapi.types.chat.abstracts.PublicChat |  | ||||||
| import dev.inmo.tgbotapi.types.dice.SlotMachineDiceAnimationType |  | ||||||
| import dev.inmo.tgbotapi.types.message.abstracts.* |  | ||||||
| import dev.inmo.tgbotapi.types.message.content.abstracts.MessageContent |  | ||||||
| import kotlinx.coroutines.* | import kotlinx.coroutines.* | ||||||
| import kotlinx.coroutines.channels.Channel |  | ||||||
| import kotlinx.coroutines.channels.toList |  | ||||||
| import kotlinx.serialization.Serializable | import kotlinx.serialization.Serializable | ||||||
|  | import kotlinx.serialization.json.JsonObject | ||||||
| import org.jetbrains.exposed.sql.Database | import org.jetbrains.exposed.sql.Database | ||||||
|  | import org.koin.core.Koin | ||||||
|  | import org.koin.core.module.Module | ||||||
|  | import org.koin.core.qualifier.named | ||||||
|  | import org.koin.dsl.binds | ||||||
|  |  | ||||||
| private const val enableAutoDeleteCommands = "captcha_auto_delete_commands_on" | private const val enableAutoDeleteCommands = "captcha_auto_delete_commands_on" | ||||||
| private const val disableAutoDeleteCommands = "captcha_auto_delete_commands_off" | private const val disableAutoDeleteCommands = "captcha_auto_delete_commands_off" | ||||||
|  | private const val enableAutoDeleteServiceMessages = "captcha_auto_delete_events_on" | ||||||
|  | private const val disableAutoDeleteServiceMessages = "captcha_auto_delete_events_off" | ||||||
|  |  | ||||||
|  | private const val enableSlotMachineCaptcha = "captcha_use_slot_machine" | ||||||
|  | private const val enableSimpleCaptcha = "captcha_use_simple" | ||||||
|  | private const val enableExpressionCaptcha = "captcha_use_expression" | ||||||
|  | private const val disableCaptcha = "disable_captcha" | ||||||
|  | private const val enableCaptcha = "enable_captcha" | ||||||
|  |  | ||||||
|  | private val enableDisableKickOnUnsuccess = Regex("captcha_(enable|disable)_kick") | ||||||
|  | private const val enableKickOnUnsuccess = "captcha_enable_kick" | ||||||
|  | private const val disableKickOnUnsuccess = "captcha_disable_kick" | ||||||
|  | private const val enableCAS = "captcha_enable_cas" | ||||||
|  | private const val disableCAS = "captcha_disable_cas" | ||||||
|  |  | ||||||
|  | private val changeCaptchaMethodCommandRegex = Regex( | ||||||
|  |     "captcha_use_((slot_machine)|(simple)|(expression))" | ||||||
|  | ) | ||||||
|  |  | ||||||
| @Serializable | @Serializable | ||||||
| class CaptchaBotPlugin : Plugin { | class CaptchaBotPlugin : Plugin { | ||||||
|     override suspend fun getCommands(): List<BotCommand> = listOf( |  | ||||||
|         BotCommand( |  | ||||||
|             enableAutoDeleteCommands, |  | ||||||
|             "Enable auto removing of commands addressed to captcha plugin" |  | ||||||
|         ), |  | ||||||
|         BotCommand( |  | ||||||
|             disableAutoDeleteCommands, |  | ||||||
|             "Disable auto removing of commands addressed to captcha plugin" |  | ||||||
|         ) |  | ||||||
|     ) |  | ||||||
|  |  | ||||||
|     override suspend fun BehaviourContext.invoke( |     override fun Module.setupDI(database: Database, params: JsonObject) { | ||||||
|         database: Database, |         single { CaptchaChatsSettingsRepo(database) } | ||||||
|         params: Map<String, Any> |  | ||||||
|     ) { |         single(named(uuid4().toString())) { | ||||||
|         val repo = CaptchaChatsSettingsRepo(database) |             BotCommandFullInfo( | ||||||
|         val adminsAPI = params.adminsPlugin ?.adminsAPI(database) |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(enableAutoDeleteCommands, "Enable auto removing of commands addressed to captcha plugin") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(disableAutoDeleteCommands, "Disable auto removing of commands addressed to captcha plugin") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(enableAutoDeleteServiceMessages, "Enable auto removing of users joined messages") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(disableAutoDeleteServiceMessages, "Disable auto removing of users joined messages") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(enableSlotMachineCaptcha, "Change captcha method to slot machine") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(enableSimpleCaptcha, "Change captcha method to simple button") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(disableCaptcha, "Disable captcha for chat") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(enableCaptcha, "Enable captcha for chat") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(enableExpressionCaptcha, "Change captcha method to expressions") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(enableKickOnUnsuccess, "Not solved captcha users will be kicked from the chat") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(disableKickOnUnsuccess, "Not solved captcha users will NOT be kicked from the chat") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(enableCAS, "Users banned in CAS will fail captcha automatically") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single(named(uuid4().toString())) { | ||||||
|  |             BotCommandFullInfo( | ||||||
|  |                 CommandsKeeperKey(BotCommandScope.AllChatAdministrators), | ||||||
|  |                 BotCommand(disableCAS, "Users banned in CAS will NOT fail captcha automatically") | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |         single { | ||||||
|  |             KtorCASChecker( | ||||||
|  |                 HttpClient(), | ||||||
|  |                 get() | ||||||
|  |             ) | ||||||
|  |         } binds arrayOf( | ||||||
|  |             CASChecker::class | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun BehaviourContext.setupBotPlugin(koin: Koin) { | ||||||
|  |         val repo: CaptchaChatsSettingsRepo by koin.inject() | ||||||
|  |         val adminsAPI = koin.getOrNull<AdminsCacheAPI>() | ||||||
|  |         val casChecker = koin.get<CASChecker>() | ||||||
|  |  | ||||||
|         suspend fun Chat.settings() = repo.getById(id) ?: repo.create(ChatSettings(id)).first() |         suspend fun Chat.settings() = repo.getById(id) ?: repo.create(ChatSettings(id)).first() | ||||||
|  |  | ||||||
|         onNewChatMembers( |         onNewChatMembers( | ||||||
|             additionalFilter = { |             initialFilter = { | ||||||
|                 it.chat.asPublicChat() != null |                 it.chat is GroupChat | ||||||
|             }, |             } | ||||||
|             includeFilterByChatInBehaviourSubContext = false |         ) { msg -> | ||||||
|         ) { |             val settings = msg.chat.settings() | ||||||
|             safelyWithoutExceptions { deleteMessage(it) } |             if (!settings.enabled) return@onNewChatMembers | ||||||
|             val eventDateTime = it.date |  | ||||||
|             val chat = it.chat.requirePublicChat() |             safelyWithoutExceptions { | ||||||
|             val newUsers = it.chatEvent.members |                 if (settings.autoRemoveEvents) { | ||||||
|  |                     deleteMessage(msg) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             val chat = msg.chat.groupChatOrThrow() | ||||||
|  |             var newUsers = msg.chatEvent.members | ||||||
|             newUsers.forEach { user -> |             newUsers.forEach { user -> | ||||||
|                 restrictChatMember( |                 restrictChatMember( | ||||||
|                     chat, |                     chat, | ||||||
|                     user, |                     user, | ||||||
|                     permissions = ChatPermissions() |                     permissions = RestrictionsChatPermissions | ||||||
|                 ) |                 ) | ||||||
|             } |             } | ||||||
|             val settings = it.chat.settings() ?: return@onNewChatMembers |             newUsers = if (settings.casEnabled) { | ||||||
|             val userBanDateTime = eventDateTime + settings.checkTimeSpan |                 newUsers.filterNot { user -> | ||||||
|             val authorized = Channel<User>(newUsers.size) |                     casChecker.isBanned(user.id).also { isBanned -> | ||||||
|             val messagesToDelete = Channel<Message>(Channel.UNLIMITED) |                         runCatchingSafely { | ||||||
|             val subContexts = newUsers.map { |                             if (isBanned) { | ||||||
|                 doInSubContext(stopOnCompletion = false) { |                                 reply( | ||||||
|                     val sentMessage = sendTextMessage( |                                     msg | ||||||
|                         chat, |                                 ) { | ||||||
|                         buildEntities { |                                     +"User " + mention(user) + " is banned in " + link("CAS System", "https://cas.chat/query?u=${user.id.chatId}") | ||||||
|                             +it.mention(it.firstName) |                                 } | ||||||
|                             regular(", ${settings.captchaText}") |                                 if (settings.kickOnUnsuccess) { | ||||||
|  |                                     banChatMember(msg.chat.id, user) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                     ).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<String?>(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 { |             } else { | ||||||
|                                 safelyWithoutExceptions { answerCallbackQuery(userClicked, "Nope") } |                 newUsers | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         authorized.send(it) |  | ||||||
|                         safelyWithoutExceptions { restrictChatMember(chat, it, permissions = LeftRestrictionsChatPermissions) } |  | ||||||
|                         stop() |  | ||||||
|             } |             } | ||||||
|  |             val defaultChatPermissions = LeftRestrictionsChatPermissions | ||||||
|  |  | ||||||
|                     this to it |             with (settings.captchaProvider) { | ||||||
|                 } |                 doAction(msg.date, chat, newUsers, defaultChatPermissions, adminsAPI, settings.kickOnUnsuccess) | ||||||
|             } |  | ||||||
|  |  | ||||||
|             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) |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (adminsAPI != null) { |         if (adminsAPI != null) { | ||||||
|  |             onCommand(changeCaptchaMethodCommandRegex) { | ||||||
|  |                 it.doAfterVerification(adminsAPI) { | ||||||
|  |                     val settings = it.chat.settings() | ||||||
|  |                     if (settings.autoRemoveCommands) { | ||||||
|  |                         safelyWithoutExceptions { deleteMessage(it) } | ||||||
|  |                     } | ||||||
|  |                     val commands = it.parseCommandsWithParams() | ||||||
|  |                     val changeCommand = commands.keys.first { | ||||||
|  |                         println(it) | ||||||
|  |                         changeCaptchaMethodCommandRegex.matches(it) | ||||||
|  |                     } | ||||||
|  |                     println(changeCommand) | ||||||
|  |                     val captcha = when { | ||||||
|  |                         changeCommand.startsWith(enableSimpleCaptcha) -> SimpleCaptchaProvider() | ||||||
|  |                         changeCommand.startsWith(enableExpressionCaptcha) -> ExpressionCaptchaProvider() | ||||||
|  |                         changeCommand.startsWith(enableSlotMachineCaptcha) -> SlotMachineCaptchaProvider() | ||||||
|  |                         else -> return@doAfterVerification | ||||||
|  |                     } | ||||||
|  |                     val newSettings = settings.copy(captchaProvider = captcha) | ||||||
|  |                     if (repo.contains(it.chat.id)) { | ||||||
|  |                         repo.update(it.chat.id, newSettings) | ||||||
|  |                     } else { | ||||||
|  |                         repo.create(newSettings) | ||||||
|  |                     } | ||||||
|  |                     sendMessage(it.chat, "Settings updated").also { sent -> | ||||||
|  |                         delay(5000L) | ||||||
|  |  | ||||||
|  |                         if (settings.autoRemoveCommands) { | ||||||
|  |                             deleteMessage(sent) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|             onCommand( |             onCommand( | ||||||
|                 enableAutoDeleteCommands, |                 enableAutoDeleteCommands, | ||||||
|                 requireOnlyCommandInMessage = false |                 requireOnlyCommandInMessage = false | ||||||
| @@ -181,6 +270,162 @@ class CaptchaBotPlugin : Plugin { | |||||||
|                     ) |                     ) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             onCommand(disableCaptcha) { message -> | ||||||
|  |                 message.doAfterVerification(adminsAPI) { | ||||||
|  |                     val settings = message.chat.settings() | ||||||
|  |  | ||||||
|  |                     repo.update( | ||||||
|  |                         message.chat.id, | ||||||
|  |                         settings.copy(enabled = false) | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     reply(message, "Captcha has been disabled") | ||||||
|  |  | ||||||
|  |                     if (settings.autoRemoveCommands) { | ||||||
|  |                         deleteMessage(message) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             onCommand(enableCaptcha) { message -> | ||||||
|  |                 message.doAfterVerification(adminsAPI) { | ||||||
|  |                     val settings = message.chat.settings() | ||||||
|  |  | ||||||
|  |                     repo.update( | ||||||
|  |                         message.chat.id, | ||||||
|  |                         settings.copy(enabled = true) | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     reply(message, "Captcha has been enabled") | ||||||
|  |  | ||||||
|  |                     if (settings.autoRemoveCommands) { | ||||||
|  |                         deleteMessage(message) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             onCommand(enableAutoDeleteServiceMessages) { message -> | ||||||
|  |                 message.doAfterVerification(adminsAPI) { | ||||||
|  |                     val settings = message.chat.settings() | ||||||
|  |  | ||||||
|  |                     repo.update( | ||||||
|  |                         message.chat.id, | ||||||
|  |                         settings.copy(autoRemoveEvents = true) | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     reply(message, "Ok, user joined service messages will be deleted") | ||||||
|  |  | ||||||
|  |                     if (settings.autoRemoveCommands) { | ||||||
|  |                         deleteMessage(message) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             onCommand(disableAutoDeleteServiceMessages) { message -> | ||||||
|  |                 message.doAfterVerification(adminsAPI) { | ||||||
|  |                     val settings = message.chat.settings() | ||||||
|  |  | ||||||
|  |                     repo.update( | ||||||
|  |                         message.chat.id, | ||||||
|  |                         settings.copy(autoRemoveEvents = false) | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     reply(message, "Ok, user joined service messages will not be deleted") | ||||||
|  |  | ||||||
|  |                     if (settings.autoRemoveCommands) { | ||||||
|  |                         deleteMessage(message) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             onCommand(enableKickOnUnsuccess) { message -> | ||||||
|  |                 message.doAfterVerification(adminsAPI) { | ||||||
|  |                     val settings = message.chat.settings() | ||||||
|  |  | ||||||
|  |                     repo.update( | ||||||
|  |                         message.chat.id, | ||||||
|  |                         settings.copy(kickOnUnsuccess = true) | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     reply(message, "Ok, new users didn't passed captcha will be kicked").apply { | ||||||
|  |                         launchSafelyWithoutExceptions { | ||||||
|  |                             delay(5000L) | ||||||
|  |                             delete(this@apply) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (settings.autoRemoveCommands) { | ||||||
|  |                         deleteMessage(message) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             onCommand(disableKickOnUnsuccess) { message -> | ||||||
|  |                 message.doAfterVerification(adminsAPI) { | ||||||
|  |                     val settings = message.chat.settings() | ||||||
|  |  | ||||||
|  |                     repo.update( | ||||||
|  |                         message.chat.id, | ||||||
|  |                         settings.copy(kickOnUnsuccess = false) | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     reply(message, "Ok, new users didn't passed captcha will NOT be kicked").apply { | ||||||
|  |                         launchSafelyWithoutExceptions { | ||||||
|  |                             delay(5000L) | ||||||
|  |                             delete(this@apply) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (settings.autoRemoveCommands) { | ||||||
|  |                         deleteMessage(message) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             onCommand(enableCAS) { message -> | ||||||
|  |                 message.doAfterVerification(adminsAPI) { | ||||||
|  |                     val settings = message.chat.settings() | ||||||
|  |  | ||||||
|  |                     repo.update( | ||||||
|  |                         message.chat.id, | ||||||
|  |                         settings.copy(casEnabled = true) | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     reply(message, "Ok, CAS banned user will automatically fail captcha").apply { | ||||||
|  |                         launchSafelyWithoutExceptions { | ||||||
|  |                             delay(5000L) | ||||||
|  |                             delete(this@apply) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (settings.autoRemoveCommands) { | ||||||
|  |                         deleteMessage(message) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             onCommand(disableCAS) { message -> | ||||||
|  |                 message.doAfterVerification(adminsAPI) { | ||||||
|  |                     val settings = message.chat.settings() | ||||||
|  |  | ||||||
|  |                     repo.update( | ||||||
|  |                         message.chat.id, | ||||||
|  |                         settings.copy(casEnabled = false) | ||||||
|  |                     ) | ||||||
|  |  | ||||||
|  |                     reply(message, "Ok, CAS banned user will NOT automatically fail captcha").apply { | ||||||
|  |                         launchSafelyWithoutExceptions { | ||||||
|  |                             delay(5000L) | ||||||
|  |                             delete(this@apply) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     if (settings.autoRemoveCommands) { | ||||||
|  |                         deleteMessage(message) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,29 +1,30 @@ | |||||||
| package dev.inmo.plagubot.plugins.captcha | package dev.inmo.plagubot.plugins.captcha | ||||||
|  |  | ||||||
|  | import dev.inmo.plagubot.plugins.captcha.provider.cancelData | ||||||
| import dev.inmo.tgbotapi.extensions.utils.SlotMachineReelImage | import dev.inmo.tgbotapi.extensions.utils.SlotMachineReelImage | ||||||
|  | import dev.inmo.tgbotapi.extensions.utils.types.buttons.* | ||||||
|  | import dev.inmo.tgbotapi.libraries.cache.admins.AdminsCacheAPI | ||||||
| import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton | import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton | ||||||
|  | import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.InlineKeyboardButton | ||||||
| import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup | import dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup | ||||||
|  |  | ||||||
| infix fun String.startingOf(target: String) = target.startsWith(this) | infix fun String.startingOf(target: String) = target.startsWith(this) | ||||||
|  |  | ||||||
|  | private val buttonsPreset: List<List<InlineKeyboardButton>> = SlotMachineReelImage.values().toList().chunked(2).map { | ||||||
|  |     it.map { | ||||||
|  |         CallbackDataInlineKeyboardButton(it.text, it.text) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| fun slotMachineReplyMarkup( | fun slotMachineReplyMarkup( | ||||||
|     first: String? = null, |     adminCancelButton: Boolean = false | ||||||
|     second: String? = null, |  | ||||||
|     third: String? = null, |  | ||||||
| ): InlineKeyboardMarkup { | ): InlineKeyboardMarkup { | ||||||
|     val texts = when { |     return inlineKeyboard { | ||||||
|         first == null -> SlotMachineReelImage.values().map { |         buttonsPreset.forEach(::add) | ||||||
|             CallbackDataInlineKeyboardButton("${it.text}**", it.text) |         if (adminCancelButton) { | ||||||
|  |             row { | ||||||
|  |                 dataButton("Cancel (Admins only)", cancelData) | ||||||
|             } |             } | ||||||
|         second == null -> SlotMachineReelImage.values().map { |  | ||||||
|             CallbackDataInlineKeyboardButton("$first${it.text}*", it.text) |  | ||||||
|         } |         } | ||||||
|         third == null -> SlotMachineReelImage.values().map { |  | ||||||
|             CallbackDataInlineKeyboardButton("$first$second${it.text}", it.text) |  | ||||||
|     } |     } | ||||||
|         else -> listOf(CallbackDataInlineKeyboardButton("$first$second$third", "$first$second$third")) |  | ||||||
|     } |  | ||||||
|     return InlineKeyboardMarkup( |  | ||||||
|         texts.chunked(2) |  | ||||||
|     ) |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,7 @@ | |||||||
|  | package dev.inmo.plagubot.plugins.captcha.cas | ||||||
|  |  | ||||||
|  | import dev.inmo.tgbotapi.types.UserId | ||||||
|  |  | ||||||
|  | interface CASChecker { | ||||||
|  |     suspend fun isBanned(userId: UserId): Boolean | ||||||
|  | } | ||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | package dev.inmo.plagubot.plugins.captcha.cas | ||||||
|  |  | ||||||
|  | import dev.inmo.tgbotapi.types.UserId | ||||||
|  | import io.ktor.client.HttpClient | ||||||
|  | import io.ktor.client.call.body | ||||||
|  | import io.ktor.client.request.get | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlinx.serialization.json.Json | ||||||
|  |  | ||||||
|  | class KtorCASChecker( | ||||||
|  |     private val httpClient: HttpClient, | ||||||
|  |     private val json: Json | ||||||
|  | ) : CASChecker { | ||||||
|  |     @Serializable | ||||||
|  |     private data class CheckResponse( | ||||||
|  |         val ok: Boolean | ||||||
|  |     ) | ||||||
|  |     override suspend fun isBanned(userId: UserId): Boolean = httpClient.get( | ||||||
|  |         "https://api.cas.chat/check?user_id=${userId.chatId}" | ||||||
|  |     ).body<String>().let { | ||||||
|  |         json.decodeFromString(CheckResponse.serializer(), it) | ||||||
|  |     }.ok | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,13 +1,25 @@ | |||||||
| package dev.inmo.plagubot.plugins.captcha.db | package dev.inmo.plagubot.plugins.captcha.db | ||||||
|  |  | ||||||
|  | import dev.inmo.micro_utils.coroutines.launchSynchronously | ||||||
| import dev.inmo.micro_utils.repos.exposed.* | import dev.inmo.micro_utils.repos.exposed.* | ||||||
| import dev.inmo.micro_utils.repos.exposed.keyvalue.ExposedKeyValueRepo | import dev.inmo.micro_utils.repos.versions.VersionsRepo | ||||||
|  | import dev.inmo.plagubot.plugins.captcha.provider.CaptchaProvider | ||||||
|  | import dev.inmo.plagubot.plugins.captcha.provider.SimpleCaptchaProvider | ||||||
| import dev.inmo.plagubot.plugins.captcha.settings.* | import dev.inmo.plagubot.plugins.captcha.settings.* | ||||||
| import dev.inmo.tgbotapi.types.ChatId | import dev.inmo.tgbotapi.types.ChatId | ||||||
| import dev.inmo.tgbotapi.types.toChatId | import dev.inmo.tgbotapi.types.toChatId | ||||||
|  | import kotlinx.coroutines.CoroutineScope | ||||||
|  | import kotlinx.serialization.json.Json | ||||||
| import org.jetbrains.exposed.sql.* | import org.jetbrains.exposed.sql.* | ||||||
| import org.jetbrains.exposed.sql.statements.InsertStatement | import org.jetbrains.exposed.sql.statements.InsertStatement | ||||||
| import org.jetbrains.exposed.sql.statements.UpdateStatement | import org.jetbrains.exposed.sql.statements.UpdateStatement | ||||||
|  | import org.jetbrains.exposed.sql.transactions.transaction | ||||||
|  |  | ||||||
|  | private val captchaProviderSerialFormat = Json { | ||||||
|  |     ignoreUnknownKeys = true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private val defaultCaptchaProviderValue = captchaProviderSerialFormat.encodeToString(CaptchaProvider.serializer(), SimpleCaptchaProvider()) | ||||||
|  |  | ||||||
| class CaptchaChatsSettingsRepo( | class CaptchaChatsSettingsRepo( | ||||||
|     override val database: Database |     override val database: Database | ||||||
| @@ -15,47 +27,57 @@ class CaptchaChatsSettingsRepo( | |||||||
|     tableName = "CaptchaChatsSettingsRepo" |     tableName = "CaptchaChatsSettingsRepo" | ||||||
| ) { | ) { | ||||||
|     private val chatIdColumn = long("chatId") |     private val chatIdColumn = long("chatId") | ||||||
|     private val checkTimeSecondsColumn = integer("checkTime") |     private val captchaProviderColumn = text("captchaProvider").apply { | ||||||
|     private val solveCaptchaTextColumn = text("solveCaptchaText") |         default(defaultCaptchaProviderValue) | ||||||
|  |     } | ||||||
|     private val autoRemoveCommandsColumn = bool("autoRemoveCommands") |     private val autoRemoveCommandsColumn = bool("autoRemoveCommands") | ||||||
|  |     private val autoRemoveEventsColumn = bool("autoRemoveEvents").apply { default(true) } | ||||||
|  |     private val enabledColumn = bool("enabled").default(true) | ||||||
|  |     private val kickOnUnsuccessColumn = bool("kick").default(true) | ||||||
|  |  | ||||||
|     override val primaryKey = PrimaryKey(chatIdColumn) |     override val primaryKey = PrimaryKey(chatIdColumn) | ||||||
|  |  | ||||||
|     override val selectByIds: SqlExpressionBuilder.(List<ChatId>) -> Op<Boolean> = { |     override val selectByIds: SqlExpressionBuilder.(List<ChatId>) -> Op<Boolean> = { | ||||||
|         chatIdColumn.inList(it.map { it.chatId }) |         chatIdColumn.inList(it.map { it.chatId }) | ||||||
|     } |     } | ||||||
|     override val InsertStatement<Number>.asObject: ChatSettings |  | ||||||
|         get() = TODO("Not yet implemented") |  | ||||||
|  |  | ||||||
|     override fun insert(value: ChatSettings, it: InsertStatement<Number>) { |     override fun insert(value: ChatSettings, it: InsertStatement<Number>) { | ||||||
|         it[chatIdColumn] = value.chatId.chatId |         it[chatIdColumn] = value.chatId.chatId | ||||||
|         it[checkTimeSecondsColumn] = value.checkTime |         it[captchaProviderColumn] = captchaProviderSerialFormat.encodeToString(CaptchaProvider.serializer(), value.captchaProvider) | ||||||
|         it[solveCaptchaTextColumn] = value.captchaText |  | ||||||
|         it[autoRemoveCommandsColumn] = value.autoRemoveCommands |         it[autoRemoveCommandsColumn] = value.autoRemoveCommands | ||||||
|  |         it[autoRemoveEventsColumn] = value.autoRemoveEvents | ||||||
|  |         it[enabledColumn] = value.enabled | ||||||
|  |         it[kickOnUnsuccessColumn] = value.kickOnUnsuccess | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun update(id: ChatId, value: ChatSettings, it: UpdateStatement) { |     override fun update(id: ChatId, value: ChatSettings, it: UpdateStatement) { | ||||||
|         if (id.chatId == value.chatId.chatId) { |         if (id.chatId == value.chatId.chatId) { | ||||||
|             it[checkTimeSecondsColumn] = value.checkTime |             it[captchaProviderColumn] = captchaProviderSerialFormat.encodeToString(CaptchaProvider.serializer(), value.captchaProvider) | ||||||
|             it[solveCaptchaTextColumn] = value.captchaText |  | ||||||
|             it[autoRemoveCommandsColumn] = value.autoRemoveCommands |             it[autoRemoveCommandsColumn] = value.autoRemoveCommands | ||||||
|  |             it[autoRemoveEventsColumn] = value.autoRemoveEvents | ||||||
|  |             it[enabledColumn] = value.enabled | ||||||
|  |             it[kickOnUnsuccessColumn] = value.kickOnUnsuccess | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun InsertStatement<Number>.asObject(value: ChatSettings): ChatSettings = ChatSettings( |     override fun InsertStatement<Number>.asObject(value: ChatSettings): ChatSettings = ChatSettings( | ||||||
|         get(chatIdColumn).toChatId(), |         chatId = get(chatIdColumn).toChatId(), | ||||||
|         get(checkTimeSecondsColumn), |         captchaProvider = captchaProviderSerialFormat.decodeFromString(CaptchaProvider.serializer(), get(captchaProviderColumn)), | ||||||
|         get(solveCaptchaTextColumn), |         autoRemoveCommands = get(autoRemoveCommandsColumn), | ||||||
|         get(autoRemoveCommandsColumn) |         autoRemoveEvents = get(autoRemoveEventsColumn), | ||||||
|  |         enabled = get(enabledColumn), | ||||||
|  |         kickOnUnsuccess = get(kickOnUnsuccessColumn) | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     override val selectById: SqlExpressionBuilder.(ChatId) -> Op<Boolean> = { chatIdColumn.eq(it.chatId) } |     override val selectById: SqlExpressionBuilder.(ChatId) -> Op<Boolean> = { chatIdColumn.eq(it.chatId) } | ||||||
|     override val ResultRow.asObject: ChatSettings |     override val ResultRow.asObject: ChatSettings | ||||||
|         get() = ChatSettings( |         get() = ChatSettings( | ||||||
|             get(chatIdColumn).toChatId(), |             chatId = get(chatIdColumn).toChatId(), | ||||||
|             get(checkTimeSecondsColumn), |             captchaProvider = captchaProviderSerialFormat.decodeFromString(CaptchaProvider.serializer(), get(captchaProviderColumn)), | ||||||
|             get(solveCaptchaTextColumn), |             autoRemoveCommands = get(autoRemoveCommandsColumn), | ||||||
|             get(autoRemoveCommandsColumn) |             autoRemoveEvents = get(autoRemoveEventsColumn), | ||||||
|  |             enabled = get(enabledColumn), | ||||||
|  |             kickOnUnsuccess = get(kickOnUnsuccessColumn) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|     init { |     init { | ||||||
|   | |||||||
| @@ -0,0 +1,506 @@ | |||||||
|  | package dev.inmo.plagubot.plugins.captcha.provider | ||||||
|  |  | ||||||
|  | import com.benasher44.uuid.uuid4 | ||||||
|  | import com.soywiz.klock.* | ||||||
|  | import dev.inmo.kslog.common.e | ||||||
|  | import dev.inmo.kslog.common.logger | ||||||
|  | import dev.inmo.micro_utils.coroutines.* | ||||||
|  | import dev.inmo.plagubot.plugins.captcha.slotMachineReplyMarkup | ||||||
|  | import dev.inmo.tgbotapi.extensions.api.answers.answer | ||||||
|  | import dev.inmo.tgbotapi.extensions.api.answers.answerCallbackQuery | ||||||
|  | import dev.inmo.tgbotapi.extensions.api.chat.members.* | ||||||
|  | import dev.inmo.tgbotapi.extensions.api.delete | ||||||
|  | import dev.inmo.tgbotapi.extensions.api.deleteMessage | ||||||
|  | import dev.inmo.tgbotapi.extensions.api.edit.edit | ||||||
|  | import dev.inmo.tgbotapi.extensions.api.edit.reply_markup.editMessageReplyMarkup | ||||||
|  | import dev.inmo.tgbotapi.extensions.api.send.* | ||||||
|  | import dev.inmo.tgbotapi.extensions.behaviour_builder.* | ||||||
|  | import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitMessageDataCallbackQuery | ||||||
|  | import dev.inmo.tgbotapi.extensions.utils.asSlotMachineReelImage | ||||||
|  | import dev.inmo.tgbotapi.extensions.utils.calculateSlotMachineResult | ||||||
|  | import dev.inmo.tgbotapi.extensions.utils.extensions.sameMessage | ||||||
|  | import dev.inmo.tgbotapi.extensions.utils.shortcuts.executeUnsafe | ||||||
|  | import dev.inmo.tgbotapi.extensions.utils.shortcuts.sentMessages | ||||||
|  | import dev.inmo.tgbotapi.extensions.utils.types.buttons.* | ||||||
|  | import dev.inmo.tgbotapi.libraries.cache.admins.AdminsCacheAPI | ||||||
|  | import dev.inmo.tgbotapi.requests.DeleteMessage | ||||||
|  | import dev.inmo.tgbotapi.types.* | ||||||
|  | import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton | ||||||
|  | import dev.inmo.tgbotapi.types.chat.ChatPermissions | ||||||
|  | import dev.inmo.tgbotapi.types.chat.* | ||||||
|  | import dev.inmo.tgbotapi.types.chat.User | ||||||
|  | import dev.inmo.tgbotapi.types.dice.SlotMachineDiceAnimationType | ||||||
|  | import dev.inmo.tgbotapi.types.message.abstracts.Message | ||||||
|  | import dev.inmo.tgbotapi.utils.* | ||||||
|  | import dev.inmo.tgbotapi.utils.EntitiesBuilder | ||||||
|  | import dev.inmo.tgbotapi.utils.bold | ||||||
|  | import dev.inmo.tgbotapi.utils.regular | ||||||
|  | import kotlinx.coroutines.* | ||||||
|  | import kotlinx.coroutines.channels.Channel | ||||||
|  | import kotlinx.coroutines.channels.toList | ||||||
|  | import kotlinx.coroutines.flow.* | ||||||
|  | import kotlinx.coroutines.sync.Mutex | ||||||
|  | import kotlinx.coroutines.sync.withLock | ||||||
|  | import kotlinx.serialization.Serializable | ||||||
|  | import kotlinx.serialization.Transient | ||||||
|  | import kotlin.random.Random | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | sealed class CaptchaProvider { | ||||||
|  |     abstract val checkTimeSpan: TimeSpan | ||||||
|  |  | ||||||
|  |     interface CaptchaProviderWorker { | ||||||
|  |         suspend fun BehaviourContext.doCaptcha(): Boolean | ||||||
|  |  | ||||||
|  |         suspend fun BehaviourContext.onCloseCaptcha(passed: Boolean) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     protected abstract suspend fun allocateWorker( | ||||||
|  |         eventDateTime: DateTime, | ||||||
|  |         chat: GroupChat, | ||||||
|  |         user: User, | ||||||
|  |         leftRestrictionsPermissions: ChatPermissions, | ||||||
|  |         adminsApi: AdminsCacheAPI?, | ||||||
|  |         kickOnUnsuccess: Boolean | ||||||
|  |     ): CaptchaProviderWorker | ||||||
|  |  | ||||||
|  |     suspend fun BehaviourContext.doAction( | ||||||
|  |         eventDateTime: DateTime, | ||||||
|  |         chat: GroupChat, | ||||||
|  |         newUsers: List<User>, | ||||||
|  |         leftRestrictionsPermissions: ChatPermissions, | ||||||
|  |         adminsApi: AdminsCacheAPI?, | ||||||
|  |         kickOnUnsuccess: Boolean | ||||||
|  |     ) { | ||||||
|  |         val userBanDateTime = eventDateTime + checkTimeSpan | ||||||
|  |         newUsers.map { user -> | ||||||
|  |             launch { | ||||||
|  |                 createSubContextAndDoWithUpdatesFilter { | ||||||
|  |                     val worker = allocateWorker( | ||||||
|  |                         eventDateTime, | ||||||
|  |                         chat, | ||||||
|  |                         user, | ||||||
|  |                         leftRestrictionsPermissions, | ||||||
|  |                         adminsApi, | ||||||
|  |                         kickOnUnsuccess | ||||||
|  |                     ) | ||||||
|  |                     val deferred = async { | ||||||
|  |                         runCatchingSafely { | ||||||
|  |                             with(worker) { | ||||||
|  |                                 doCaptcha() | ||||||
|  |                             } | ||||||
|  |                         }.onFailure { | ||||||
|  |                             this@CaptchaProvider.logger.e("Unable to do captcha", it) | ||||||
|  |                         }.getOrElse { false } | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     val subscope = LinkedSupervisorScope() | ||||||
|  |                     subscope.launch { | ||||||
|  |                         delay((userBanDateTime - eventDateTime).millisecondsLong) | ||||||
|  |                         subscope.cancel() | ||||||
|  |                     } | ||||||
|  |                     subscope.launch { | ||||||
|  |                         deferred.await() | ||||||
|  |                         subscope.cancel() | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     subscope.coroutineContext.job.join() | ||||||
|  |  | ||||||
|  |                     val passed = runCatching { | ||||||
|  |                         deferred.getCompleted() | ||||||
|  |                     }.onFailure { | ||||||
|  |                         deferred.cancel() | ||||||
|  |                     }.getOrElse { false } | ||||||
|  |  | ||||||
|  |                     when { | ||||||
|  |                         passed -> { | ||||||
|  |                             safelyWithoutExceptions { | ||||||
|  |                                 restrictChatMember( | ||||||
|  |                                     chat, | ||||||
|  |                                     user, | ||||||
|  |                                     permissions = leftRestrictionsPermissions | ||||||
|  |                                 ) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         else -> { | ||||||
|  |                             send(chat, " ") { | ||||||
|  |                                 +"User" + mention(user) + underline("didn't passed") + "captcha" | ||||||
|  |                             } | ||||||
|  |                             if (kickOnUnsuccess) { | ||||||
|  |                                 banUser(chat, user, leftRestrictionsPermissions) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     with(worker) { | ||||||
|  |                         onCloseCaptcha(passed) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }.joinAll() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | internal const val cancelData = "cancel" | ||||||
|  |  | ||||||
|  | private fun EntitiesBuilder.mention(user: User, defaultName: String = "User"): EntitiesBuilder { | ||||||
|  |     return mention( | ||||||
|  |         listOfNotNull( | ||||||
|  |             user.lastName.takeIf { it.isNotBlank() }, user.firstName.takeIf { it.isNotBlank() } | ||||||
|  |         ).takeIf { | ||||||
|  |             it.isNotEmpty() | ||||||
|  |         }?.joinToString(" ") ?: defaultName, | ||||||
|  |         user | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private suspend fun BehaviourContext.sendAdminCanceledMessage( | ||||||
|  |     chat: Chat, | ||||||
|  |     captchaSolver: User, | ||||||
|  |     admin: User | ||||||
|  | ) { | ||||||
|  |     safelyWithoutExceptions { | ||||||
|  |         send( | ||||||
|  |             chat | ||||||
|  |         ) { | ||||||
|  |             mention(admin, "Admin") | ||||||
|  |             regular(" cancelled captcha for ") | ||||||
|  |             mention(captchaSolver) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private suspend fun BehaviourContext.banUser( | ||||||
|  |     chat: PublicChat, | ||||||
|  |     user: User, | ||||||
|  |     leftRestrictionsPermissions: ChatPermissions, | ||||||
|  |     onFailure: suspend BehaviourContext.(Throwable) -> Unit = { | ||||||
|  |         safelyWithResult { | ||||||
|  |             send( | ||||||
|  |                 chat | ||||||
|  |             ) { | ||||||
|  |                 mention(user) | ||||||
|  |                 +"failed captcha" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | ): Result<Boolean> = safelyWithResult { | ||||||
|  |     restrictChatMember(chat, user, permissions = leftRestrictionsPermissions) | ||||||
|  |     banChatMember(chat, user) | ||||||
|  | }.onFailure { | ||||||
|  |     onFailure(it) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | data class SlotMachineCaptchaProvider( | ||||||
|  |     val checkTimeSeconds: Seconds = 60, | ||||||
|  |     val captchaText: String = "Solve this captcha: " | ||||||
|  | ) : CaptchaProvider() { | ||||||
|  |     @Transient | ||||||
|  |     override val checkTimeSpan = checkTimeSeconds.seconds | ||||||
|  |  | ||||||
|  |     private inner class Worker( | ||||||
|  |         private val chat: GroupChat, | ||||||
|  |         private val user: User, | ||||||
|  |         private val adminsApi: AdminsCacheAPI? | ||||||
|  |     ) : CaptchaProviderWorker { | ||||||
|  |         private val messagesToDelete = mutableListOf<Message>() | ||||||
|  |  | ||||||
|  |         override suspend fun BehaviourContext.doCaptcha(): Boolean { | ||||||
|  |             val baseBuilder: EntitiesBuilderBody = { | ||||||
|  |                 mention(user) | ||||||
|  |                 regular(", $captchaText") | ||||||
|  |             } | ||||||
|  |             val sentMessage = send( | ||||||
|  |                 chat | ||||||
|  |             ) { | ||||||
|  |                 baseBuilder() | ||||||
|  |                 +": ✖✖✖" | ||||||
|  |             }.also { messagesToDelete.add(it) } | ||||||
|  |             val sentDice = sendDice( | ||||||
|  |                 sentMessage.chat, | ||||||
|  |                 SlotMachineDiceAnimationType, | ||||||
|  |                 replyToMessageId = sentMessage.messageId, | ||||||
|  |                 replyMarkup = slotMachineReplyMarkup(adminsApi != null) | ||||||
|  |             ).also { messagesToDelete.add(it) } | ||||||
|  |             val reels = sentDice.content.dice.calculateSlotMachineResult()!! | ||||||
|  |             val leftToClick = mutableListOf( | ||||||
|  |                 reels.left.asSlotMachineReelImage.text, | ||||||
|  |                 reels.center.asSlotMachineReelImage.text, | ||||||
|  |                 reels.right.asSlotMachineReelImage.text | ||||||
|  |             ) | ||||||
|  |             val clicked = mutableListOf<String>() | ||||||
|  |             fun buildTemplate() = "${clicked.joinToString("")}${leftToClick.joinToString("") { "✖" }}" | ||||||
|  |  | ||||||
|  |             waitMessageDataCallbackQuery().filter { | ||||||
|  |                 when { | ||||||
|  |                     !it.message.sameMessage(sentDice) -> false | ||||||
|  |                     it.data == cancelData && adminsApi ?.isAdmin(chat.id, it.user.id) == true -> return@filter true | ||||||
|  |                     it.data == cancelData && adminsApi ?.isAdmin(chat.id, it.user.id) != true -> { | ||||||
|  |                         answer( | ||||||
|  |                             it, | ||||||
|  |                             "This button is only for admins" | ||||||
|  |                         ) | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                     it.user.id != user.id -> { | ||||||
|  |                         answer(it, "This button is not for you") | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                     it.data != leftToClick.first() -> { | ||||||
|  |                         answer(it, "Nope") | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                     else -> { | ||||||
|  |                         clicked.add(leftToClick.removeFirst()) | ||||||
|  |                         answer(it, "Ok, next one") | ||||||
|  |                         edit(sentMessage) { | ||||||
|  |                             baseBuilder() | ||||||
|  |                             +": ${buildTemplate()}" | ||||||
|  |                         } | ||||||
|  |                         leftToClick.isEmpty() | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }.first() | ||||||
|  |  | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override suspend fun BehaviourContext.onCloseCaptcha(passed: Boolean) { | ||||||
|  |             while (messagesToDelete.isNotEmpty()) { | ||||||
|  |                 runCatchingSafely { delete(messagesToDelete.removeFirst()) } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun allocateWorker( | ||||||
|  |         eventDateTime: DateTime, | ||||||
|  |         chat: GroupChat, | ||||||
|  |         user: User, | ||||||
|  |         leftRestrictionsPermissions: ChatPermissions, | ||||||
|  |         adminsApi: AdminsCacheAPI?, | ||||||
|  |         kickOnUnsuccess: Boolean | ||||||
|  |     ): CaptchaProviderWorker = Worker(chat, user, adminsApi) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | data class SimpleCaptchaProvider( | ||||||
|  |     val checkTimeSeconds: Seconds = 60, | ||||||
|  |     val captchaText: String = "press this button to pass captcha:", | ||||||
|  |     val buttonText: String = "Press me\uD83D\uDE0A" | ||||||
|  | ) : CaptchaProvider() { | ||||||
|  |     @Transient | ||||||
|  |     override val checkTimeSpan = checkTimeSeconds.seconds | ||||||
|  |  | ||||||
|  |     private inner class Worker( | ||||||
|  |         private val chat: GroupChat, | ||||||
|  |         private val user: User, | ||||||
|  |         private val adminsApi: AdminsCacheAPI? | ||||||
|  |     ) : CaptchaProviderWorker { | ||||||
|  |         private var sentMessage: Message? = null | ||||||
|  |         override suspend fun BehaviourContext.doCaptcha(): Boolean { | ||||||
|  |             val callbackData = uuid4().toString() | ||||||
|  |             val sentMessage = send( | ||||||
|  |                 chat, | ||||||
|  |                 replyMarkup = inlineKeyboard { | ||||||
|  |                     row { | ||||||
|  |                         dataButton(buttonText, callbackData) | ||||||
|  |                     } | ||||||
|  |                     if (adminsApi != null) { | ||||||
|  |                         row { | ||||||
|  |                             dataButton("Cancel (Admins only)", cancelData) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ) { | ||||||
|  |                 mention(user) | ||||||
|  |                 regular(", $captchaText") | ||||||
|  |             } | ||||||
|  |             this@Worker.sentMessage = sentMessage | ||||||
|  |  | ||||||
|  |             val pushed = waitMessageDataCallbackQuery().filter { | ||||||
|  |                 when { | ||||||
|  |                     !it.message.sameMessage(sentMessage) -> false | ||||||
|  |                     it.data == callbackData && it.user.id == user.id -> true | ||||||
|  |                     it.data == cancelData && (adminsApi ?.isAdmin(chat.id, it.user.id) == true) -> true | ||||||
|  |                     it.data == callbackData -> { | ||||||
|  |                         answer(it, "This button is not for you") | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                     it.data == cancelData -> { | ||||||
|  |                         answer(it, "This button is for admins only") | ||||||
|  |                         false | ||||||
|  |                     } | ||||||
|  |                     else -> false | ||||||
|  |                 } | ||||||
|  |             }.first() | ||||||
|  |  | ||||||
|  |             answer( | ||||||
|  |                 pushed, | ||||||
|  |                 when (pushed.data) { | ||||||
|  |                     cancelData -> "You have cancelled captcha" | ||||||
|  |                     else -> "Ok, thanks" | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |             return true | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override suspend fun BehaviourContext.onCloseCaptcha(passed: Boolean) { | ||||||
|  |             sentMessage ?.let { | ||||||
|  |                 delete(it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun allocateWorker( | ||||||
|  |         eventDateTime: DateTime, | ||||||
|  |         chat: GroupChat, | ||||||
|  |         user: User, | ||||||
|  |         leftRestrictionsPermissions: ChatPermissions, | ||||||
|  |         adminsApi: AdminsCacheAPI?, | ||||||
|  |         kickOnUnsuccess: Boolean | ||||||
|  |     ): CaptchaProviderWorker = Worker(chat, user, adminsApi) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | private object ExpressionBuilder { | ||||||
|  |     sealed class ExpressionOperation { | ||||||
|  |         object PlusExpressionOperation : ExpressionOperation() { | ||||||
|  |             override fun asString(): String = "+" | ||||||
|  |  | ||||||
|  |             override fun Int.perform(other: Int): Int = plus(other) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         object MinusExpressionOperation : ExpressionOperation() { | ||||||
|  |             override fun asString(): String = "-" | ||||||
|  |  | ||||||
|  |             override fun Int.perform(other: Int): Int = minus(other) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         abstract fun asString(): String | ||||||
|  |         abstract fun Int.perform(other: Int): Int | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private val experssions = | ||||||
|  |         listOf(ExpressionOperation.PlusExpressionOperation, ExpressionOperation.MinusExpressionOperation) | ||||||
|  |  | ||||||
|  |     private fun createNumber(max: Int) = Random.nextInt(max + 1) | ||||||
|  |     fun generateResult(max: Int, operationsNumber: Int = 1): Int { | ||||||
|  |         val operations = (0 until operationsNumber).map { experssions.random() } | ||||||
|  |         var current = createNumber(max) | ||||||
|  |         operations.forEach { | ||||||
|  |             val rightOne = createNumber(max) | ||||||
|  |             current = it.run { current.perform(rightOne) } | ||||||
|  |         } | ||||||
|  |         return current | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun createExpression(max: Int, operationsNumber: Int = 1): Pair<Int, String> { | ||||||
|  |         val operations = (0 until operationsNumber).map { experssions.random() } | ||||||
|  |         var current = createNumber(max) | ||||||
|  |         var numbersString = "$current" | ||||||
|  |         operations.forEach { | ||||||
|  |             val rightOne = createNumber(max) | ||||||
|  |             current = it.run { current.perform(rightOne) } | ||||||
|  |             numbersString += " ${it.asString()} $rightOne" | ||||||
|  |         } | ||||||
|  |         return current to numbersString | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @Serializable | ||||||
|  | data class ExpressionCaptchaProvider( | ||||||
|  |     val checkTimeSeconds: Seconds = 60, | ||||||
|  |     val captchaText: String = "Solve next captcha:", | ||||||
|  |     val leftRetriesText: String = "Nope, left retries: ", | ||||||
|  |     val maxPerNumber: Int = 10, | ||||||
|  |     val operations: Int = 2, | ||||||
|  |     val answers: Int = 6, | ||||||
|  |     val attempts: Int = 3 | ||||||
|  | ) : CaptchaProvider() { | ||||||
|  |     @Transient | ||||||
|  |     override val checkTimeSpan = checkTimeSeconds.seconds | ||||||
|  |  | ||||||
|  |     private inner class Worker( | ||||||
|  |         private val chat: GroupChat, | ||||||
|  |         private val user: User, | ||||||
|  |         private val adminsApi: AdminsCacheAPI? | ||||||
|  |     ) : CaptchaProviderWorker { | ||||||
|  |         private var sentMessage: Message? = null | ||||||
|  |         override suspend fun BehaviourContext.doCaptcha(): Boolean { | ||||||
|  |             val callbackData = ExpressionBuilder.createExpression( | ||||||
|  |                 maxPerNumber, | ||||||
|  |                 operations | ||||||
|  |             ) | ||||||
|  |             val correctAnswer = callbackData.first.toString() | ||||||
|  |             val answers = (0 until answers - 1).map { | ||||||
|  |                 ExpressionBuilder.generateResult(maxPerNumber, operations) | ||||||
|  |             }.toMutableList().also { orderedAnswers -> | ||||||
|  |                 val correctAnswerPosition = Random.nextInt(orderedAnswers.size) | ||||||
|  |                 orderedAnswers.add(correctAnswerPosition, callbackData.first) | ||||||
|  |             }.toList() | ||||||
|  |             val sentMessage = send( | ||||||
|  |                 chat, | ||||||
|  |                 replyMarkup = inlineKeyboard { | ||||||
|  |                     answers.map { | ||||||
|  |                         CallbackDataInlineKeyboardButton(it.toString(), it.toString()) | ||||||
|  |                     }.chunked(3).forEach(::add) | ||||||
|  |                     if (adminsApi != null) { | ||||||
|  |                         row { | ||||||
|  |                             dataButton("Cancel (Admins only)", cancelData) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             ) { | ||||||
|  |                 mention(user) | ||||||
|  |                 regular(", $captchaText ") | ||||||
|  |                 bold(callbackData.second) | ||||||
|  |             }.also { | ||||||
|  |                 sentMessage = it | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             var leftAttempts = attempts | ||||||
|  |             return waitMessageDataCallbackQuery().takeWhile { leftAttempts > 0 }.map { query -> | ||||||
|  |                 val baseCheck = query.message.messageId == sentMessage.messageId | ||||||
|  |                 val dataCorrect = (query.user.id == user.id && query.data == correctAnswer) | ||||||
|  |                 val adminCanceled = (query.data == cancelData && (adminsApi?.isAdmin( | ||||||
|  |                     sentMessage.chat.id, | ||||||
|  |                     query.user.id | ||||||
|  |                 )) == true) | ||||||
|  |                 baseCheck && if (dataCorrect || adminCanceled) { | ||||||
|  |                     if (adminCanceled) { | ||||||
|  |                         sendAdminCanceledMessage( | ||||||
|  |                             sentMessage.chat, | ||||||
|  |                             user, | ||||||
|  |                             query.user | ||||||
|  |                         ) | ||||||
|  |                     } | ||||||
|  |                     true | ||||||
|  |                 } else { | ||||||
|  |                     leftAttempts-- | ||||||
|  |                     if (leftAttempts > 0) { | ||||||
|  |                         answerCallbackQuery(query, leftRetriesText + leftAttempts) | ||||||
|  |                     } | ||||||
|  |                     false | ||||||
|  |                 } | ||||||
|  |             }.firstOrNull() ?: false | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         override suspend fun BehaviourContext.onCloseCaptcha(passed: Boolean) { | ||||||
|  |             sentMessage ?.let { | ||||||
|  |                 delete(it) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override suspend fun allocateWorker( | ||||||
|  |         eventDateTime: DateTime, | ||||||
|  |         chat: GroupChat, | ||||||
|  |         user: User, | ||||||
|  |         leftRestrictionsPermissions: ChatPermissions, | ||||||
|  |         adminsApi: AdminsCacheAPI?, | ||||||
|  |         kickOnUnsuccess: Boolean | ||||||
|  |     ): CaptchaProviderWorker = Worker(chat, user, adminsApi) | ||||||
|  | } | ||||||
|  |  | ||||||
| @@ -1,18 +1,17 @@ | |||||||
| package dev.inmo.plagubot.plugins.captcha.settings | 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.ChatId | ||||||
| import dev.inmo.tgbotapi.types.Seconds |  | ||||||
| import kotlinx.serialization.Serializable | import kotlinx.serialization.Serializable | ||||||
| import kotlinx.serialization.Transient |  | ||||||
|  |  | ||||||
| @Serializable | @Serializable | ||||||
| data class ChatSettings( | data class ChatSettings( | ||||||
|     val chatId: ChatId, |     val chatId: ChatId, | ||||||
|     val checkTime: Seconds = 60, |     val captchaProvider: CaptchaProvider = SimpleCaptchaProvider(), | ||||||
|     val captchaText: String = "solve next captcha:", |     val autoRemoveCommands: Boolean = false, | ||||||
|     val autoRemoveCommands: Boolean = false |     val autoRemoveEvents: Boolean = true, | ||||||
| ) { |     val kickOnUnsuccess: Boolean = true, | ||||||
|     @Transient |     val enabled: Boolean = true, | ||||||
|     val checkTimeSpan = TimeSpan(checkTime * 1000.0) |     val casEnabled: Boolean = false | ||||||
| } | ) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user