From 9ea06de27c52a34d59679cef2968bc2a1e1737ed Mon Sep 17 00:00:00 2001 From: InsanusMokrassar Date: Wed, 24 Aug 2022 15:03:54 +0600 Subject: [PATCH] add waitCommands* expectations --- .../expectations/WaitCommandsMessages.kt | 113 ++++++++++++++++++ .../HandleableTriggersHolder.kt | 23 +++- .../kotlin/dev/inmo/tgbotapi/types/Common.kt | 2 + .../textsources/BotCommandTextSource.kt | 4 + .../extensions/utils/FlowsAggregation.kt | 2 +- 5 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitCommandsMessages.kt diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitCommandsMessages.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitCommandsMessages.kt new file mode 100644 index 0000000000..002f5ef291 --- /dev/null +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitCommandsMessages.kt @@ -0,0 +1,113 @@ +package dev.inmo.tgbotapi.extensions.behaviour_builder.expectations + +import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext +import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.handlers_registrar.doWithRegistration +import dev.inmo.tgbotapi.extensions.utils.* +import dev.inmo.tgbotapi.requests.abstracts.Request +import dev.inmo.tgbotapi.types.message.abstracts.CommonMessage +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.message.textsources.BotCommandTextSource +import dev.inmo.tgbotapi.types.message.textsources.TextSource +import kotlinx.coroutines.flow.* + +/** + * Will filter all the messages and include required commands with [commandRegex]. + * + * * In case you wish to get only the commands at the start of message, use [requireCommandAtStart] + * * In case you wish to exclude messages with more than one command, you may use [requireSingleCommand] + * * In case you wish to exclude messages with commands params, you may use [requireCommandsWithoutParams] + */ +suspend fun BehaviourContext.waitCommandMessage( + commandRegex: Regex, + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = channelFlow { + triggersHolder.handleableCommandsHolder.doWithRegistration( + commandRegex + ) { + waitTextMessage(initRequest, errorFactory).filter { + it.content.textSources.any { it.botCommandTextSourceOrNull() ?.command ?.matches(commandRegex) == true } + }.collect { + send(it) + } + } +} + +fun Flow>.requireCommandAtStart() = filter { + (it.content.textSources.firstOrNull() as? BotCommandTextSource) != null +} + +/** + * Subsequent [Flow] will retrieve only messages with ONE [BotCommandTextSource]. It does not guarantee that this + * [BotCommandTextSource] will be at the start of the message + * + * @see requireCommandAtStart + */ +fun Flow>.requireSingleCommand() = filter { + var count = 0 + + it.content.textSources.forEach { + if (it is BotCommandTextSource) { + count++ + if (count > 1) { + return@filter false + } + } + } + + true +} + +/** + * Subsequent [Flow] will retrieve only messages without [TextContent.textSources] which are not [BotCommandTextSource] + */ +fun Flow>.requireCommandsWithoutParams() = filter { + it.content.textSources.none { it !is BotCommandTextSource } +} + +/** + * Map the commands with their arguments and source messages + */ +fun Flow>.commandsWithParams(): Flow, List>>>> = mapNotNull { + var currentCommandTextSource: BotCommandTextSource? = null + val currentArgs = mutableListOf() + val result = mutableListOf>>() + + fun addCurrentCommandToResult() { + currentCommandTextSource ?.let { + result.add(it to currentArgs.toTypedArray()) + currentArgs.clear() + } + } + + it.content.textSources.forEach { + it.ifBotCommandTextSource { + addCurrentCommandToResult() + currentCommandTextSource = it + return@forEach + } + currentArgs.add(it) + } + addCurrentCommandToResult() + + result.toList().takeIf { it.isNotEmpty() } ?.let { result -> + it to result + } +} + +/** + * Flat [commandsWithParams]. Each [Pair] of [BotCommandTextSource] and its [Array] of arg text sources will + * be associated with its source message + */ +fun Flow>.flattenCommandsWithParams() = commandsWithParams().flatMapConcat { (message, commandsWithParams) -> + commandsWithParams.map { + message to it + }.asFlow() +} + +/** + * Use [flattenCommandsWithParams] and filter out the commands which do not [matches] to [commandRegex] + */ +fun Flow>.commandParams(commandRegex: Regex) = flattenCommandsWithParams().filter { (_, commandWithParams) -> + commandWithParams.first.command.matches(commandRegex) +} diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/utils/handlers_registrar/HandleableTriggersHolder.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/utils/handlers_registrar/HandleableTriggersHolder.kt index a182a7eced..784dd25654 100644 --- a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/utils/handlers_registrar/HandleableTriggersHolder.kt +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/utils/handlers_registrar/HandleableTriggersHolder.kt @@ -1,5 +1,6 @@ package dev.inmo.tgbotapi.extensions.behaviour_builder.utils.handlers_registrar +import dev.inmo.micro_utils.coroutines.runCatchingSafely import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -7,6 +8,7 @@ open class HandleableTriggersHolder( preset: List = emptyList() ) { protected val commandsMutex = Mutex() + protected val handleableCounts = mutableMapOf() protected val _handleable = mutableListOf().also { it.addAll(preset) } @@ -16,12 +18,31 @@ open class HandleableTriggersHolder( suspend fun registerHandleable(data: T) { commandsMutex.withLock { _handleable.add(data) + handleableCounts[data] = (handleableCounts[data] ?: 0) + 1 } } suspend fun unregisterHandleable(data: T) { commandsMutex.withLock { - _handleable.remove(data) + val newHandleableCount = (handleableCounts[data] ?: 0) - 1 + if (newHandleableCount > 0) { + handleableCounts[data] = newHandleableCount + } else { + handleableCounts.remove(data) + _handleable.remove(data) + } } } } + +suspend fun HandleableTriggersHolder.doWithRegistration( + data: T, + block: suspend () -> R +): R { + registerHandleable(data) + val result = runCatchingSafely { + block() + } + unregisterHandleable(data) + return result.getOrThrow() +} diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt index 0e1cf5f246..4d4c2583a1 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/Common.kt @@ -81,6 +81,8 @@ sealed interface StickerType { } } +val usernameRegex = Regex("@[\\w\\d_]+") + val degreesLimit = 1 .. 360 val horizontalAccuracyLimit = 0F .. 1500F diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BotCommandTextSource.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BotCommandTextSource.kt index b58bd11f4f..e2d711ea66 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BotCommandTextSource.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/types/message/textsources/BotCommandTextSource.kt @@ -1,5 +1,6 @@ package dev.inmo.tgbotapi.types.message.textsources +import dev.inmo.tgbotapi.types.usernameRegex import dev.inmo.tgbotapi.utils.RiskFeature import dev.inmo.tgbotapi.utils.internal.* import kotlinx.serialization.Serializable @@ -16,6 +17,9 @@ data class BotCommandTextSource @RiskFeature(DirectInvocationOfTextSourceConstru val command: String by lazy { commandRegex.find(source) ?.value ?.substring(1) ?: source.substring(1)// skip first symbol like "/" or "!" } + val username: String? by lazy { + usernameRegex.find(source) ?.value ?.substring(1) ?: source.substring(1)// skip first symbol "@" + } override val markdown: String by lazy { source.commandMarkdown() } override val markdownV2: String by lazy { source.commandMarkdownV2() } diff --git a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/FlowsAggregation.kt b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/FlowsAggregation.kt index 82cd9cb9b2..3da9f65694 100644 --- a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/FlowsAggregation.kt +++ b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/FlowsAggregation.kt @@ -30,7 +30,7 @@ fun Flow>.flatten(): Flow = flow { } } -fun Flow.flatMap(mapper: (T) -> Iterable): Flow = flow { +fun Flow.flatMap(mapper: suspend (T) -> Iterable): Flow = flow { collect { mapper(it).forEach { emit(it)