Compare commits

30 Commits

Author SHA1 Message Date
43cdef96d0 Update gradle.properties 2022-05-17 00:27:59 +06:00
7d76814a41 Update gradle.properties 2022-05-04 14:51:04 +06:00
4fbb752b8f update telegram bot api library 2022-02-02 13:51:49 +06:00
306eccf380 add defaults to new columns 2022-01-05 01:16:10 +06:00
d375875067 chats settings update 2022-01-05 01:15:32 +06:00
0e097fc9ba add several feedback replies for plugin 2022-01-05 01:05:02 +06:00
852262853e add several opportunities in plugin 2022-01-05 00:48:35 +06:00
88047703ad Update gradle.properties 2022-01-04 18:59:34 +06:00
867a2b6fe5 updates 2021-11-13 20:01:20 +06:00
f4019f67e2 Update gradle.properties 2021-11-13 00:32:21 +06:00
6235837cee Update gradle.properties 2021-11-12 17:13:32 +06:00
f952db018e Update gradle.properties 2021-09-22 23:53:51 +06:00
5c7d9dce05 Update gradle.properties 2021-07-03 14:39:25 +06:00
4a0e2cc843 now on kicks will restrict users too 2021-06-13 02:00:03 +06:00
5e52e2c32e actualization 2021-06-13 01:35:47 +06:00
98f07d6611 update dependencies 2021-05-06 11:57:41 +06:00
0cb1b45c0e Update gradle.properties 2021-05-01 20:42:28 +06:00
9fa72f8716 Update gradle.properties 2021-04-25 15:43:18 +06:00
6fe5f96e4e updates 2021-04-05 23:34:54 +06:00
338e97770d update dependencis 2021-04-05 22:09:33 +06:00
1520a670c4 Update gradle.properties 2021-04-04 01:41:23 +06:00
3a7ef56565 now user captcha checking is in parallel 2021-04-03 14:22:01 +06:00
4a7339afd9 fixes 2021-03-31 00:02:18 +06:00
1d4baa8be9 add setting up of captcha method 2021-03-30 22:51:34 +06:00
b3a9a9875f add expression provider 2021-03-30 21:05:26 +06:00
8cc2503934 small update 2021-03-28 15:31:54 +06:00
87a6cab33e fixes in simple captcha provider 2021-03-28 15:18:11 +06:00
f84edb7860 fix in db 2021-03-25 07:07:04 +06:00
506f319c88 experimentally add CaptchaProvider 2021-03-25 06:53:42 +06:00
b09bcd1d75 start 0.1.6 2021-03-25 06:51:15 +06:00
8 changed files with 623 additions and 135 deletions

View File

@@ -8,7 +8,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 1.8
java-version: 11
- name: Update version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"

View File

@@ -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")
}
}
}
}

View File

@@ -4,13 +4,13 @@ org.gradle.parallel=true
kotlin.js.generate.externals=true
kotlin.incremental=true
kotlin_version=1.4.31
kotlin_coroutines_version=1.4.3
kotlin_serialisation_runtime_version=1.1.0
plagubot_version=0.1.5
kotlin_version=1.6.21
kotlin_coroutines_version=1.6.1
kotlin_serialisation_runtime_version=1.3.3
plagubot_version=1.0.0
micro_utils_version=0.4.30
tgbotapi_libraries_version=0.0.2-branch_master-build12
micro_utils_version=0.10.4
tgbotapi_libraries_version=0.0.18
project_group=dev.inmo
project_version=0.1.5
project_version=0.1.6

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -4,45 +4,45 @@ import dev.inmo.micro_utils.coroutines.*
import dev.inmo.micro_utils.repos.create
import dev.inmo.plagubot.Plugin
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.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.extensions.api.answers.answerCallbackQuery
import dev.inmo.tgbotapi.extensions.api.chat.get.getChat
import dev.inmo.tgbotapi.extensions.api.chat.members.*
import dev.inmo.tgbotapi.extensions.api.deleteMessage
import dev.inmo.tgbotapi.extensions.api.edit.ReplyMarkup.editMessageReplyMarkup
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.api.send.*
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.updateshandlers.FlowsUpdatesFilter
import dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling.onNewChatMembers
import dev.inmo.tgbotapi.extensions.utils.*
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.extensions.parseCommandsWithParams
import dev.inmo.tgbotapi.extensions.utils.extensions.sourceChat
import dev.inmo.tgbotapi.libraries.cache.admins.*
import dev.inmo.tgbotapi.requests.DeleteMessage
import dev.inmo.tgbotapi.types.BotCommand
import dev.inmo.tgbotapi.types.MessageEntity.textsources.mention
import dev.inmo.tgbotapi.types.User
import dev.inmo.tgbotapi.types.chat.ChatPermissions
import dev.inmo.tgbotapi.types.chat.LeftRestrictionsChatPermissions
import dev.inmo.tgbotapi.types.chat.RestrictionsChatPermissions
import dev.inmo.tgbotapi.types.chat.abstracts.Chat
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 dev.inmo.tgbotapi.types.chat.abstracts.extended.ExtendedGroupChat
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.toList
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.Database
private const val enableAutoDeleteCommands = "captcha_auto_delete_commands_on"
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 val changeCaptchaMethodCommandRegex = Regex(
"captcha_use_((slot_machine)|(simple)|(expression))"
)
@Serializable
class CaptchaBotPlugin : Plugin {
@@ -54,6 +54,42 @@ class CaptchaBotPlugin : Plugin {
BotCommand(
disableAutoDeleteCommands,
"Disable auto removing of commands addressed to captcha plugin"
),
BotCommand(
enableAutoDeleteServiceMessages,
"Enable auto removing of users joined messages"
),
BotCommand(
disableAutoDeleteServiceMessages,
"Disable auto removing of users joined messages"
),
BotCommand(
enableSlotMachineCaptcha,
"Change captcha method to slot machine"
),
BotCommand(
enableSimpleCaptcha,
"Change captcha method to simple button"
),
BotCommand(
disableCaptcha,
"Disable captcha for chat"
),
BotCommand(
enableCaptcha,
"Enable captcha for chat"
),
BotCommand(
enableExpressionCaptcha,
"Change captcha method to expressions"
),
BotCommand(
enableKickOnUnsuccess,
"Not solved captcha users will be kicked from the chat"
),
BotCommand(
disableKickOnUnsuccess,
"Not solved captcha users will NOT be kicked from the chat"
)
)
@@ -66,93 +102,74 @@ class CaptchaBotPlugin : Plugin {
suspend fun Chat.settings() = repo.getById(id) ?: repo.create(ChatSettings(id)).first()
onNewChatMembers(
additionalFilter = {
initialFilter = {
it.chat.asPublicChat() != null
},
includeFilterByChatInBehaviourSubContext = false
subcontextUpdatesFilter = { m, u -> u.sourceChat() == m.chat },
) {
safelyWithoutExceptions { deleteMessage(it) }
val eventDateTime = it.date
val chat = it.chat.requirePublicChat()
val newUsers = it.chatEvent.members
newUsers.forEach { user ->
restrictChatMember(
chat,
user,
permissions = ChatPermissions()
)
}
val settings = it.chat.settings() ?: return@onNewChatMembers
val userBanDateTime = eventDateTime + settings.checkTimeSpan
val authorized = Channel<User>(newUsers.size)
val messagesToDelete = Channel<Message>(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
)
launchSafelyWithoutExceptions {
val settings = it.chat.settings()
if (!settings.enabled) return@launchSafelyWithoutExceptions
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 {
safelyWithoutExceptions { answerCallbackQuery(userClicked, "Nope") }
}
}
authorized.send(it)
safelyWithoutExceptions { restrictChatMember(chat, it, permissions = LeftRestrictionsChatPermissions) }
stop()
safelyWithoutExceptions {
if (settings.autoRemoveEvents) {
deleteMessage(it)
}
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) }
val chat = it.chat.requireGroupChat()
val newUsers = it.chatEvent.members
newUsers.forEach { user ->
restrictChatMember(
chat,
user,
permissions = RestrictionsChatPermissions
)
}
val defaultChatPermissions = (getChat(it.chat) as ExtendedGroupChat).permissions
doInSubContext(stopOnCompletion = false) {
launch {
settings.captchaProvider.apply { doAction(it.date, chat, newUsers, defaultChatPermissions) }
}
}
}
messagesToDelete.close()
for (message in messagesToDelete) {
executeUnsafe(DeleteMessage(message.chat.id, message.messageId), retries = 0)
}
}
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(
enableAutoDeleteCommands,
requireOnlyCommandInMessage = false
@@ -181,6 +198,74 @@ 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)
}
}
}
}
}
}

View File

@@ -1,13 +1,25 @@
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.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.tgbotapi.types.ChatId
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.statements.InsertStatement
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(
override val database: Database
@@ -15,47 +27,52 @@ class CaptchaChatsSettingsRepo(
tableName = "CaptchaChatsSettingsRepo"
) {
private val chatIdColumn = long("chatId")
private val checkTimeSecondsColumn = integer("checkTime")
private val solveCaptchaTextColumn = text("solveCaptchaText")
private val captchaProviderColumn = text("captchaProvider").apply {
default(defaultCaptchaProviderValue)
}
private val autoRemoveCommandsColumn = bool("autoRemoveCommands")
private val autoRemoveEventsColumn = bool("autoRemoveEvents").apply { default(true) }
private val enabledColumn = bool("enabled").default(true)
override val primaryKey = PrimaryKey(chatIdColumn)
override val selectByIds: SqlExpressionBuilder.(List<ChatId>) -> Op<Boolean> = {
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>) {
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
it[autoRemoveEventsColumn] = value.autoRemoveEvents
it[enabledColumn] = value.enabled
}
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
it[autoRemoveEventsColumn] = value.autoRemoveEvents
it[enabledColumn] = value.enabled
}
}
override fun InsertStatement<Number>.asObject(value: ChatSettings): ChatSettings = ChatSettings(
get(chatIdColumn).toChatId(),
get(checkTimeSecondsColumn),
get(solveCaptchaTextColumn),
get(autoRemoveCommandsColumn)
chatId = get(chatIdColumn).toChatId(),
captchaProvider = captchaProviderSerialFormat.decodeFromString(CaptchaProvider.serializer(), get(captchaProviderColumn)),
autoRemoveCommands = get(autoRemoveCommandsColumn),
autoRemoveEvents = get(autoRemoveEventsColumn),
enabled = get(enabledColumn)
)
override val selectById: SqlExpressionBuilder.(ChatId) -> Op<Boolean> = { chatIdColumn.eq(it.chatId) }
override val ResultRow.asObject: ChatSettings
get() = ChatSettings(
get(chatIdColumn).toChatId(),
get(checkTimeSecondsColumn),
get(solveCaptchaTextColumn),
get(autoRemoveCommandsColumn)
chatId = get(chatIdColumn).toChatId(),
captchaProvider = captchaProviderSerialFormat.decodeFromString(CaptchaProvider.serializer(), get(captchaProviderColumn)),
autoRemoveCommands = get(autoRemoveCommandsColumn),
autoRemoveEvents = get(autoRemoveEventsColumn),
enabled = get(enabledColumn)
)
init {

View File

@@ -0,0 +1,381 @@
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.safelyWithResult
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.*
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.*
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.*
import dev.inmo.tgbotapi.types.MessageEntity.textsources.mention
import dev.inmo.tgbotapi.types.buttons.InlineKeyboardButtons.CallbackDataInlineKeyboardButton
import dev.inmo.tgbotapi.types.chat.ChatPermissions
import dev.inmo.tgbotapi.types.chat.LeftRestrictionsChatPermissions
import dev.inmo.tgbotapi.types.chat.abstracts.*
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.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 suspend fun BehaviourContext.doAction(
eventDateTime: DateTime,
chat: GroupChat,
newUsers: List<User>,
leftRestrictionsPermissions: ChatPermissions
)
}
private suspend fun BehaviourContext.banUser(
chat: PublicChat,
user: User,
leftRestrictionsPermissions: ChatPermissions,
onFailure: suspend BehaviourContext.(Throwable) -> Unit = {
safelyWithResult {
sendTextMessage(
chat,
buildEntities(" ") {
user.mention(
listOfNotNull(
user.lastName.takeIf { it.isNotBlank() }, user.firstName.takeIf { it.isNotBlank() }
).takeIf {
it.isNotEmpty()
} ?.joinToString(" ") ?: "User"
)
+"failed captcha"
}
)
}
}
): Result<Boolean> = safelyWithResult {
restrictChatMember(chat, user, permissions = leftRestrictionsPermissions)
banChatMember(chat, user)
}.onFailure {
onFailure(it)
}
@Serializable
data class SlotMachineCaptchaProvider(
val checkTimeSeconds: Seconds = 300,
val captchaText: String = "solve this captcha: ",
val kick: Boolean = true
) : CaptchaProvider() {
@Transient
private val checkTimeSpan = checkTimeSeconds.seconds
override suspend fun BehaviourContext.doAction(
eventDateTime: DateTime,
chat: GroupChat,
newUsers: List<User>,
leftRestrictionsPermissions: ChatPermissions
) {
val userBanDateTime = eventDateTime + checkTimeSpan
val authorized = Channel<User>(newUsers.size)
val messagesToDelete = Channel<Message>(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<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 {
safelyWithoutExceptions { answerCallbackQuery(userClicked, "Nope") }
}
}
authorized.send(it)
safelyWithoutExceptions { restrictChatMember(chat, it, permissions = leftRestrictionsPermissions) }
stop()
}
this to it
}
}
delay((userBanDateTime - eventDateTime).millisecondsLong)
authorized.close()
val authorizedUsers = authorized.toList()
subContexts.forEach { (context, user) ->
if (user !in authorizedUsers) {
context.stop()
if (kick) {
banUser(chat, user, leftRestrictionsPermissions)
}
}
}
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<User>,
leftRestrictionsPermissions: ChatPermissions
) {
val userBanDateTime = eventDateTime + checkTimeSpan
newUsers.mapNotNull {
safelyWithoutExceptions {
launch {
doInSubContext(stopOnCompletion = false) {
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 = parallel {
waitDataCallbackQuery {
if (it.id == user.id && this.data == callbackData) {
this
} else {
null
}
}.first()
removeRedundantMessages()
safelyWithoutExceptions { restrictChatMember(chat, it, permissions = leftRestrictionsPermissions) }
stop()
}
delay((userBanDateTime - eventDateTime).millisecondsLong)
if (job.isActive) {
job.cancel()
if (kick) {
banUser(chat, it, leftRestrictionsPermissions)
}
}
stop()
}
}
}
}.joinAll()
}
}
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,
val kick: Boolean = true
) : CaptchaProvider() {
@Transient
private val checkTimeSpan = checkTimeSeconds.seconds
override suspend fun BehaviourContext.doAction(
eventDateTime: DateTime,
chat: GroupChat,
newUsers: List<User>,
leftRestrictionsPermissions: ChatPermissions
) {
val userBanDateTime = eventDateTime + checkTimeSpan
newUsers.map { user ->
launch {
doInSubContext {
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 = sendTextMessage(
chat,
buildEntities {
+user.mention(user.firstName)
regular(", $captchaText ")
bold(callbackData.second)
},
replyMarkup = dev.inmo.tgbotapi.types.buttons.InlineKeyboardMarkup(
answers.map {
CallbackDataInlineKeyboardButton(it.toString(), it.toString())
}.chunked(3)
)
)
suspend fun removeRedundantMessages() {
safelyWithoutExceptions {
deleteMessage(sentMessage)
}
}
var passed: Boolean? = null
val passedMutex = Mutex()
val callback: suspend (Boolean) -> Unit = {
passedMutex.withLock {
if (passed == null) {
removeRedundantMessages()
passed = it
if (it) {
safelyWithoutExceptions { restrictChatMember(chat, user, permissions = leftRestrictionsPermissions) }
} else {
if (kick) {
banUser(chat, user, leftRestrictionsPermissions)
}
}
}
}
}
val banJob = launch {
delay((userBanDateTime - eventDateTime).millisecondsLong)
if (passed == null) {
callback(false)
stop()
}
}
var leftAttempts = attempts
waitDataCallbackQuery {
when {
this.user.id != user.id -> null
this.data != correctAnswer -> {
leftAttempts--
if (leftAttempts < 1) {
this
} else {
answerCallbackQuery(this@waitDataCallbackQuery, leftRetriesText + leftAttempts)
null
}
}
else -> this
}
}.first()
banJob.cancel()
callback(leftAttempts > 0)
}
}
}.joinAll()
}
}

View File

@@ -1,18 +1,15 @@
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 autoRemoveCommands: Boolean = false
) {
@Transient
val checkTimeSpan = TimeSpan(checkTime * 1000.0)
}
val captchaProvider: CaptchaProvider = SimpleCaptchaProvider(),
val autoRemoveCommands: Boolean = false,
val autoRemoveEvents: Boolean = true,
val enabled: Boolean = true
)