diff --git a/CHANGELOG.md b/CHANGELOG.md index bf70590d56..40b8d33b34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # TelegramBotAPI changelog +## 3.2.0 + +**Since this update, `RequestsExecutor#execute` may throw only `BotException`. In case you wish to handle some exceptions from `execute` you must catch `BotException` and handle its `cause`** + +* `Versions`: + * `Serialization`: `1.4.0-RC` -> `1.4.0` + * `MicroUtils`: `0.12.1` -> `0.12.4` +* `Core`: + * `SetWebhook#allowedUpdates` now is `ALL_UPDATES_LIST` by default instead of `null` +* `API`: + * Extension `TelegramBot#setWebhook` parameter `allowedUpdates` now is `ALL_UPDATES_LIST` by default instead of `null` +* `Utils`: + * All related to long polling extensions parameters `allowedUpdates` now are `ALL_UPDATES_LIST` by default instead of `null` + ## 3.1.1 * `Common`: diff --git a/gradle.properties b/gradle.properties index c0a1d7b5fe..93d60a6dd5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,4 @@ kotlin.incremental=true kotlin.incremental.js=true library_group=dev.inmo -library_version=3.1.1 +library_version=3.2.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0eb6cd212b..11c2a634b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "1.7.10" -kotlin-serialization = "1.4.0-RC" +kotlin-serialization = "1.4.0" kotlin-coroutines = "1.6.4" javax-activation = "1.1.1" @@ -13,7 +13,7 @@ ktor = "2.1.0" ksp = "1.7.10-1.0.6" kotlin-poet = "1.12.0" -microutils = "0.12.1" +microutils = "0.12.4" github-release-plugin = "2.4.1" diff --git a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/webhook/SetWebhookInfo.kt b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/webhook/SetWebhookInfo.kt index a0e9dcb195..d2a986c5c2 100644 --- a/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/webhook/SetWebhookInfo.kt +++ b/tgbotapi.api/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/api/webhook/SetWebhookInfo.kt @@ -4,6 +4,7 @@ import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.requests.abstracts.FileId import dev.inmo.tgbotapi.requests.abstracts.MultipartFile import dev.inmo.tgbotapi.requests.webhook.SetWebhook +import dev.inmo.tgbotapi.types.ALL_UPDATES_LIST /** * Use this method to send information about webhook (like [url]) @@ -12,7 +13,7 @@ suspend fun TelegramBot.setWebhookInfo( url: String, ipAddress: String? = null, maxAllowedConnections: Int? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, dropPendingUpdates: Boolean? = null, secretToken: String? = null ) = execute( @@ -29,7 +30,7 @@ suspend fun TelegramBot.setWebhookInfo( certificate: FileId, ipAddress: String? = null, maxAllowedConnections: Int? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, dropPendingUpdates: Boolean? = null, secretToken: String? = null ) = execute( @@ -46,7 +47,7 @@ suspend fun TelegramBot.setWebhookInfo( certificate: MultipartFile, ipAddress: String? = null, maxAllowedConnections: Int? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, dropPendingUpdates: Boolean? = null, secretToken: String? = null ) = execute( 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..df34cfab1f --- /dev/null +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitCommandsMessages.kt @@ -0,0 +1,119 @@ +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) + } + } +} + +suspend fun BehaviourContext.waitCommandMessage( + command: String, + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null } +) = waitCommandMessage(Regex(command), initRequest, errorFactory) + +fun Flow>.requireCommandAtStart() = filter { + it.content.textSources.firstOrNull() is BotCommandTextSource +} + +/** + * 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 + } + } + } + + count == 1 +} + +/** + * 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/expectations/WaitDeepLinks.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitDeepLinks.kt new file mode 100644 index 0000000000..129d7e72c1 --- /dev/null +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/expectations/WaitDeepLinks.kt @@ -0,0 +1,23 @@ +package dev.inmo.tgbotapi.extensions.behaviour_builder.expectations + +import dev.inmo.tgbotapi.extensions.behaviour_builder.BehaviourContext +import dev.inmo.tgbotapi.extensions.utils.regularTextSourceOrNull +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.RegularTextSource +import kotlinx.coroutines.flow.* + +suspend fun BehaviourContext.waitDeepLinks( + initRequest: Request<*>? = null, + errorFactory: NullableRequestBuilder<*> = { null }, +): Flow, String>> = waitCommandMessage( + "start", + initRequest, + errorFactory +) + .requireSingleCommand() + .requireCommandAtStart() + .flattenCommandsWithParams().mapNotNull { + it.first to (it.second.second.singleOrNull() ?.regularTextSourceOrNull() ?.source ?.removePrefix(" ") ?: return@mapNotNull null) + } diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/DeepLinkHandling.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/DeepLinkHandling.kt new file mode 100644 index 0000000000..cf85ec58f9 --- /dev/null +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/triggers_handling/DeepLinkHandling.kt @@ -0,0 +1,48 @@ +@file:Suppress("unused") + +package dev.inmo.tgbotapi.extensions.behaviour_builder.triggers_handling + +import dev.inmo.micro_utils.coroutines.* +import dev.inmo.tgbotapi.extensions.behaviour_builder.* +import dev.inmo.tgbotapi.extensions.behaviour_builder.expectations.waitDeepLinks +import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.CommonMessageFilterExcludeMediaGroups +import dev.inmo.tgbotapi.extensions.behaviour_builder.filters.MessageFilterByChat +import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.SimpleFilter +import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.marker_factories.ByChatMessageMarkerFactory +import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.marker_factories.MarkerFactory +import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.times +import dev.inmo.tgbotapi.extensions.utils.* +import dev.inmo.tgbotapi.extensions.utils.extensions.parseCommandsWithParams +import dev.inmo.tgbotapi.types.message.content.TextContent +import dev.inmo.tgbotapi.types.message.content.TextMessage +import dev.inmo.tgbotapi.types.message.textsources.RegularTextSource +import dev.inmo.tgbotapi.types.update.abstracts.Update +import io.ktor.http.decodeURLQueryComponent +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.filter + +private val startRegex = Regex("start") +suspend fun BC.onDeepLink( + initialFilter: SimpleFilter>? = null, + subcontextUpdatesFilter: CustomBehaviourContextAndTwoTypesReceiver, Update> = { (message, _), update -> MessageFilterByChat(this, message, update) }, + markerFactory: MarkerFactory, Any> = MarkerFactory { (message, _) -> ByChatMessageMarkerFactory(message) }, + scenarioReceiver: CustomBehaviourContextAndTypeReceiver> +): Job = on( + markerFactory, + SimpleFilter> { (message, _) -> + message.content.textSources.size == 2 + && message.content.textSources.firstOrNull() ?.asBotCommandTextSource() ?.command == "start" + && message.content.textSources.getOrNull(1) is RegularTextSource + } * initialFilter, + subcontextUpdatesFilter, + scenarioReceiver, +) { + (it.messageUpdateOrNull()) ?.data ?.commonMessageOrNull() ?.withContentOrNull() ?.let { message -> + message to message.content.textSources[1].source.removePrefix(" ").decodeURLQueryComponent() + } ?.let(::listOfNotNull) +}.also { + triggersHolder.handleableCommandsHolder.registerHandleable(startRegex) + it.invokeOnCompletion { + this@onDeepLink.launchSafelyWithoutExceptions { triggersHolder.handleableCommandsHolder.unregisterHandleable(startRegex) } + } +} diff --git a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/utils/SimpleFilter.kt b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/utils/SimpleFilter.kt index fb918ffb93..e4dac61c93 100644 --- a/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/utils/SimpleFilter.kt +++ b/tgbotapi.behaviour_builder/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/behaviour_builder/utils/SimpleFilter.kt @@ -4,6 +4,7 @@ fun interface SimpleFilter { suspend operator fun invoke(o: T): Boolean } val TrueSimpleFilter = SimpleFilter { true } +val FalseSimpleFilter = SimpleFilter { false } /** * @return [SimpleFilter] which will return true in case when all the items in incoming data passed [this] filter @@ -28,21 +29,29 @@ fun SimpleFilter.listNone() = SimpleFilter> { /** * Makes an AND (&&) operation between [this] and [other] + * + * * When both arguments are null, [TrueSimpleFilter] will be returned */ -operator fun SimpleFilter?.times(other: SimpleFilter): SimpleFilter = this ?.let { - SimpleFilter { - this(it) && other(it) - } -} ?: other +infix operator fun SimpleFilter?.times(other: SimpleFilter?): SimpleFilter = this ?.let { + other ?.let { + SimpleFilter { + this(it) && other(it) + } + } ?: it +} ?: other ?: TrueSimpleFilter /** * Makes an OR (||) operation between [this] and [other] + * + * * When both arguments are null, [TrueSimpleFilter] will be returned */ -operator fun SimpleFilter?.plus(other: SimpleFilter): SimpleFilter = this ?.let { - SimpleFilter { - this(it) || other(it) - } -} ?: other +infix operator fun SimpleFilter?.plus(other: SimpleFilter?): SimpleFilter = this ?.let { + other ?.let { + SimpleFilter { + this(it) || other(it) + } + } ?: it +} ?: other ?: TrueSimpleFilter /** * Reverse results of [this] 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/bot/exceptions/RequestException.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/exceptions/RequestException.kt index 3ed3242660..7448d2e901 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/exceptions/RequestException.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/exceptions/RequestException.kt @@ -35,13 +35,18 @@ fun newRequestException( } } ?: CommonRequestException(response, plainAnswer, message, cause) +sealed class BotException(message: String = "Something went wrong", cause: Throwable? = null) : IOException(message, cause) + +class CommonBotException(message: String = "Something went wrong", cause: Throwable? = null) : BotException(message, cause) + sealed class RequestException constructor( val response: Response, val plainAnswer: String, message: String? = null, - override val cause: Throwable? = null -) : IOException( - message ?: "Something went wrong" + cause: Throwable? = null +) : BotException( + message ?: "Something went wrong", + cause ) class CommonRequestException(response: Response, plainAnswer: String, message: String?, cause: Throwable?) : diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt index 45ed64fc02..03ab290421 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutor.kt @@ -1,9 +1,10 @@ package dev.inmo.tgbotapi.bot.ktor +import dev.inmo.micro_utils.coroutines.runCatchingSafely import dev.inmo.micro_utils.coroutines.safely import dev.inmo.tgbotapi.bot.BaseRequestsExecutor import dev.inmo.tgbotapi.bot.TelegramBot -import dev.inmo.tgbotapi.bot.exceptions.newRequestException +import dev.inmo.tgbotapi.bot.exceptions.* import dev.inmo.tgbotapi.bot.ktor.base.* import dev.inmo.tgbotapi.bot.settings.limiters.ExceptionsOnlyLimiter import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter @@ -48,12 +49,36 @@ class KtorRequestsExecutor( } override suspend fun execute(request: Request): T { - return runCatching { - safely( - { e -> - pipelineStepsHolder.onRequestException(request, e) ?.let { return@safely it } + return runCatchingSafely { + pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories) + requestsLimiter.limit { + var result: T? = null + lateinit var factoryHandledRequest: KtorCallFactory + for (potentialFactory in callsFactories) { + pipelineStepsHolder.onBeforeCallFactoryMakeCall(request, potentialFactory) + result = potentialFactory.makeCall( + client, + telegramAPIUrlsKeeper, + request, + jsonFormatter + ) + result = pipelineStepsHolder.onAfterCallFactoryMakeCall(result, request, potentialFactory) + if (result != null) { + factoryHandledRequest = potentialFactory + break + } + } - throw if (e is ClientRequestException) { + result ?.let { + pipelineStepsHolder.onRequestResultPresented(it, request, factoryHandledRequest, callsFactories) + } ?: pipelineStepsHolder.onRequestResultAbsent(request, callsFactories) ?: error("Can't execute request: $request") + } + }.let { + val result = it.exceptionOrNull() ?.let { e -> + pipelineStepsHolder.onRequestException(request, e) ?.let { return@let it } + + if (e is ClientRequestException) { + val exceptionResult = runCatchingSafely { val content = e.response.bodyAsText() val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) newRequestException( @@ -61,38 +86,15 @@ class KtorRequestsExecutor( content, "Can't get result object from $content" ) - } else { - e } - + exceptionResult.exceptionOrNull() ?.let { + CommonBotException(cause = e) + } ?: exceptionResult.getOrThrow() + } else { + CommonBotException(cause = e) } - ) { - pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories) - requestsLimiter.limit { - var result: T? = null - lateinit var factoryHandledRequest: KtorCallFactory - for (potentialFactory in callsFactories) { - pipelineStepsHolder.onBeforeCallFactoryMakeCall(request, potentialFactory) - result = potentialFactory.makeCall( - client, - telegramAPIUrlsKeeper, - request, - jsonFormatter - ) - result = pipelineStepsHolder.onAfterCallFactoryMakeCall(result, request, potentialFactory) - if (result != null) { - factoryHandledRequest = potentialFactory - break - } - } - - result ?.let { - pipelineStepsHolder.onRequestResultPresented(it, request, factoryHandledRequest, callsFactories) - } ?: pipelineStepsHolder.onRequestResultAbsent(request, callsFactories) ?: error("Can't execute request: $request") - } - } - }.let { - pipelineStepsHolder.onRequestReturnResult(it, request, callsFactories) + } ?.let { Result.failure(it) } ?: it + pipelineStepsHolder.onRequestReturnResult(result, request, callsFactories) } } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/webhook/SetWebhook.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/webhook/SetWebhook.kt index 39482247c5..774e7ae80f 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/webhook/SetWebhook.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/requests/webhook/SetWebhook.kt @@ -19,7 +19,7 @@ class MultipartSetWebhookRequest( certificate: MultipartFile, ipAddress: String? = null, maxAllowedConnections: Int? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, dropPendingUpdates: Boolean? = null, secretToken: String? = null ) : SetWebhookRequest(), MultipartRequest by MultipartRequestImpl( @@ -40,7 +40,7 @@ fun SetWebhook( certificate: MultipartFile, ipAddress: String? = null, maxAllowedConnections: Int? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, dropPendingUpdates: Boolean? = null, secretToken: String? = null ): MultipartSetWebhookRequest = MultipartSetWebhookRequest( @@ -58,7 +58,7 @@ fun SetWebhook( certificate: FileId, ipAddress: String? = null, maxAllowedConnections: Int? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, dropPendingUpdates: Boolean? = null, secretToken: String? = null ): SetWebhook = SetWebhook( @@ -84,7 +84,7 @@ fun SetWebhook( certificate: InputFile, ipAddress: String? = null, maxAllowedConnections: Int? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, dropPendingUpdates: Boolean? = null, secretToken: String? = null ) = when (certificate) { @@ -104,7 +104,7 @@ fun SetWebhook( url: String, ipAddress: String? = null, maxAllowedConnections: Int? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, dropPendingUpdates: Boolean? = null, secretToken: String? = null ) = SetWebhook( @@ -135,7 +135,7 @@ data class SetWebhook internal constructor( @SerialName(maxAllowedConnectionsField) val maxAllowedConnections: Int? = null, @SerialName(allowedUpdatesField) - val allowedUpdates: List? = null, + val allowedUpdates: List? = ALL_UPDATES_LIST, @SerialName(dropPendingUpdatesField) val dropPendingUpdates: Boolean? = null, @SerialName(secretTokenField) 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..bb143c5fe0 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,7 @@ package dev.inmo.tgbotapi.types.message.textsources +import dev.inmo.tgbotapi.types.usernameRegex +import dev.inmo.tgbotapi.types.Username import dev.inmo.tgbotapi.utils.RiskFeature import dev.inmo.tgbotapi.utils.internal.* import kotlinx.serialization.Serializable @@ -16,6 +18,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: Username? by lazy { + Username(usernameRegex.find(source) ?.value ?: return@lazy null) + } 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) diff --git a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/formatting/LinksFormatting.kt b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/formatting/LinksFormatting.kt index 21ec91f1fc..3e16e1f192 100644 --- a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/formatting/LinksFormatting.kt +++ b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/formatting/LinksFormatting.kt @@ -4,6 +4,7 @@ import dev.inmo.tgbotapi.types.* import dev.inmo.tgbotapi.types.chat.* import dev.inmo.tgbotapi.types.message.abstracts.Message import dev.inmo.tgbotapi.types.message.textsources.link +import io.ktor.http.encodeURLQueryComponent fun makeUsernameLink(username: String) = "$internalLinkBeginning/$username" @@ -17,11 +18,11 @@ inline val Username.deepLinkPrefix inline val Username.startattachPrefix get() = makeUsernameStartattachPrefix(usernameWithoutAt) inline fun makeLink(username: Username) = username.link -inline fun makeTelegramDeepLink(username: String, startParameter: String) = "${makeUsernameDeepLinkPrefix(username)}$startParameter" +inline fun makeTelegramDeepLink(username: String, startParameter: String) = "${makeUsernameDeepLinkPrefix(username)}$startParameter".encodeURLQueryComponent() inline fun makeTelegramStartattach(username: String, data: String? = null) = makeUsernameStartattachLink(username, data) -inline fun makeDeepLink(username: Username, startParameter: String) = "${username.deepLinkPrefix}$startParameter" +inline fun makeDeepLink(username: Username, startParameter: String) = makeTelegramDeepLink(username.usernameWithoutAt, startParameter) inline fun makeTelegramDeepLink(username: Username, startParameter: String) = makeDeepLink(username, startParameter) -inline fun makeTelegramStartattach(username: Username, data: String? = null) = makeTelegramStartattach(username.username, data) +inline fun makeTelegramStartattach(username: Username, data: String? = null) = makeTelegramStartattach(username.usernameWithoutAt, data) fun makeLinkToMessage( username: String, diff --git a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/LongPolling.kt b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/LongPolling.kt index 421427e2ac..830f37f0c3 100644 --- a/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/LongPolling.kt +++ b/tgbotapi.utils/src/commonMain/kotlin/dev/inmo/tgbotapi/extensions/utils/updates/retrieving/LongPolling.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.* fun TelegramBot.longPollingFlow( timeoutSeconds: Seconds = 30, exceptionsHandler: (ExceptionHandler)? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, ): Flow = channelFlow { var lastUpdateIdentifier: UpdateIdentifier? = null @@ -81,7 +81,7 @@ fun TelegramBot.startGettingOfUpdatesByLongPolling( timeoutSeconds: Seconds = 30, scope: CoroutineScope = CoroutineScope(Dispatchers.Default), exceptionsHandler: (ExceptionHandler)? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, updatesReceiver: UpdateReceiver ): Job = longPollingFlow(timeoutSeconds, exceptionsHandler, allowedUpdates).subscribeSafely( scope, @@ -97,7 +97,7 @@ fun TelegramBot.createAccumulatedUpdatesRetrieverFlow( avoidInlineQueries: Boolean = false, avoidCallbackQueries: Boolean = false, exceptionsHandler: ExceptionHandler? = null, - allowedUpdates: List? = null + allowedUpdates: List? = ALL_UPDATES_LIST ): Flow = longPollingFlow( timeoutSeconds = 0, exceptionsHandler = { @@ -117,7 +117,7 @@ fun TelegramBot.retrieveAccumulatedUpdates( avoidCallbackQueries: Boolean = false, scope: CoroutineScope = CoroutineScope(Dispatchers.Default), exceptionsHandler: (ExceptionHandler)? = null, - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, updatesReceiver: UpdateReceiver ): Job = createAccumulatedUpdatesRetrieverFlow( avoidInlineQueries, @@ -149,7 +149,7 @@ suspend fun TelegramBot.flushAccumulatedUpdates( avoidInlineQueries: Boolean = false, avoidCallbackQueries: Boolean = false, scope: CoroutineScope = CoroutineScope(Dispatchers.Default), - allowedUpdates: List? = null, + allowedUpdates: List? = ALL_UPDATES_LIST, exceptionsHandler: ExceptionHandler? = null, updatesReceiver: UpdateReceiver = {} ) = retrieveAccumulatedUpdates(