1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2024-11-26 03:58:44 +00:00

Merge pull request #648 from InsanusMokrassar/3.2.0

3.2.0
This commit is contained in:
InsanusMokrassar 2022-08-26 16:36:27 +06:00 committed by GitHub
commit ae8a461e9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 321 additions and 71 deletions

View File

@ -1,5 +1,19 @@
# TelegramBotAPI changelog # 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 ## 3.1.1
* `Common`: * `Common`:

View File

@ -6,4 +6,4 @@ kotlin.incremental=true
kotlin.incremental.js=true kotlin.incremental.js=true
library_group=dev.inmo library_group=dev.inmo
library_version=3.1.1 library_version=3.2.0

View File

@ -1,7 +1,7 @@
[versions] [versions]
kotlin = "1.7.10" kotlin = "1.7.10"
kotlin-serialization = "1.4.0-RC" kotlin-serialization = "1.4.0"
kotlin-coroutines = "1.6.4" kotlin-coroutines = "1.6.4"
javax-activation = "1.1.1" javax-activation = "1.1.1"
@ -13,7 +13,7 @@ ktor = "2.1.0"
ksp = "1.7.10-1.0.6" ksp = "1.7.10-1.0.6"
kotlin-poet = "1.12.0" kotlin-poet = "1.12.0"
microutils = "0.12.1" microutils = "0.12.4"
github-release-plugin = "2.4.1" github-release-plugin = "2.4.1"

View File

@ -4,6 +4,7 @@ import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.requests.abstracts.FileId import dev.inmo.tgbotapi.requests.abstracts.FileId
import dev.inmo.tgbotapi.requests.abstracts.MultipartFile import dev.inmo.tgbotapi.requests.abstracts.MultipartFile
import dev.inmo.tgbotapi.requests.webhook.SetWebhook 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]) * Use this method to send information about webhook (like [url])
@ -12,7 +13,7 @@ suspend fun TelegramBot.setWebhookInfo(
url: String, url: String,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) = execute( ) = execute(
@ -29,7 +30,7 @@ suspend fun TelegramBot.setWebhookInfo(
certificate: FileId, certificate: FileId,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) = execute( ) = execute(
@ -46,7 +47,7 @@ suspend fun TelegramBot.setWebhookInfo(
certificate: MultipartFile, certificate: MultipartFile,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) = execute( ) = execute(

View File

@ -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<CommonMessage<TextContent>>.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<CommonMessage<TextContent>>.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<CommonMessage<TextContent>>.requireCommandsWithoutParams() = filter {
it.content.textSources.none { it !is BotCommandTextSource }
}
/**
* Map the commands with their arguments and source messages
*/
fun Flow<CommonMessage<TextContent>>.commandsWithParams(): Flow<Pair<CommonMessage<TextContent>, List<Pair<BotCommandTextSource, Array<TextSource>>>>> = mapNotNull {
var currentCommandTextSource: BotCommandTextSource? = null
val currentArgs = mutableListOf<TextSource>()
val result = mutableListOf<Pair<BotCommandTextSource, Array<TextSource>>>()
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<CommonMessage<TextContent>>.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<CommonMessage<TextContent>>.commandParams(commandRegex: Regex) = flattenCommandsWithParams().filter { (_, commandWithParams) ->
commandWithParams.first.command.matches(commandRegex)
}

View File

@ -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<Pair<CommonMessage<TextContent>, String>> = waitCommandMessage(
"start",
initRequest,
errorFactory
)
.requireSingleCommand()
.requireCommandAtStart()
.flattenCommandsWithParams().mapNotNull {
it.first to (it.second.second.singleOrNull() ?.regularTextSourceOrNull() ?.source ?.removePrefix(" ") ?: return@mapNotNull null)
}

View File

@ -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 : BehaviourContext> BC.onDeepLink(
initialFilter: SimpleFilter<Pair<TextMessage, String>>? = null,
subcontextUpdatesFilter: CustomBehaviourContextAndTwoTypesReceiver<BC, Boolean, Pair<TextMessage, String>, Update> = { (message, _), update -> MessageFilterByChat(this, message, update) },
markerFactory: MarkerFactory<Pair<TextMessage, String>, Any> = MarkerFactory { (message, _) -> ByChatMessageMarkerFactory(message) },
scenarioReceiver: CustomBehaviourContextAndTypeReceiver<BC, Unit, Pair<TextMessage, String>>
): Job = on(
markerFactory,
SimpleFilter<Pair<TextMessage, String>> { (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<TextContent>() ?.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) }
}
}

View File

@ -4,6 +4,7 @@ fun interface SimpleFilter<in T> {
suspend operator fun invoke(o: T): Boolean suspend operator fun invoke(o: T): Boolean
} }
val TrueSimpleFilter = SimpleFilter<Any?> { true } val TrueSimpleFilter = SimpleFilter<Any?> { true }
val FalseSimpleFilter = SimpleFilter<Any?> { false }
/** /**
* @return [SimpleFilter] which will return true in case when all the items in incoming data passed [this] filter * @return [SimpleFilter] which will return true in case when all the items in incoming data passed [this] filter
@ -28,21 +29,29 @@ fun <T> SimpleFilter<T>.listNone() = SimpleFilter<Iterable<T>> {
/** /**
* Makes an AND (&&) operation between [this] and [other] * Makes an AND (&&) operation between [this] and [other]
*
* * When both arguments are null, [TrueSimpleFilter] will be returned
*/ */
operator fun <T> SimpleFilter<T>?.times(other: SimpleFilter<T>): SimpleFilter<T> = this ?.let { infix operator fun <T> SimpleFilter<T>?.times(other: SimpleFilter<T>?): SimpleFilter<T> = this ?.let {
SimpleFilter { other ?.let {
this(it) && other(it) SimpleFilter {
} this(it) && other(it)
} ?: other }
} ?: it
} ?: other ?: TrueSimpleFilter
/** /**
* Makes an OR (||) operation between [this] and [other] * Makes an OR (||) operation between [this] and [other]
*
* * When both arguments are null, [TrueSimpleFilter] will be returned
*/ */
operator fun <T> SimpleFilter<T>?.plus(other: SimpleFilter<T>): SimpleFilter<T> = this ?.let { infix operator fun <T> SimpleFilter<T>?.plus(other: SimpleFilter<T>?): SimpleFilter<T> = this ?.let {
SimpleFilter { other ?.let {
this(it) || other(it) SimpleFilter {
} this(it) || other(it)
} ?: other }
} ?: it
} ?: other ?: TrueSimpleFilter
/** /**
* Reverse results of [this] * Reverse results of [this]

View File

@ -1,5 +1,6 @@
package dev.inmo.tgbotapi.extensions.behaviour_builder.utils.handlers_registrar 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.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -7,6 +8,7 @@ open class HandleableTriggersHolder<T>(
preset: List<T> = emptyList() preset: List<T> = emptyList()
) { ) {
protected val commandsMutex = Mutex() protected val commandsMutex = Mutex()
protected val handleableCounts = mutableMapOf<T, Int>()
protected val _handleable = mutableListOf<T>().also { protected val _handleable = mutableListOf<T>().also {
it.addAll(preset) it.addAll(preset)
} }
@ -16,12 +18,31 @@ open class HandleableTriggersHolder<T>(
suspend fun registerHandleable(data: T) { suspend fun registerHandleable(data: T) {
commandsMutex.withLock { commandsMutex.withLock {
_handleable.add(data) _handleable.add(data)
handleableCounts[data] = (handleableCounts[data] ?: 0) + 1
} }
} }
suspend fun unregisterHandleable(data: T) { suspend fun unregisterHandleable(data: T) {
commandsMutex.withLock { 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 <T, R> HandleableTriggersHolder<T>.doWithRegistration(
data: T,
block: suspend () -> R
): R {
registerHandleable(data)
val result = runCatchingSafely {
block()
}
unregisterHandleable(data)
return result.getOrThrow()
}

View File

@ -35,13 +35,18 @@ fun newRequestException(
} }
} ?: CommonRequestException(response, plainAnswer, message, cause) } ?: 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( sealed class RequestException constructor(
val response: Response, val response: Response,
val plainAnswer: String, val plainAnswer: String,
message: String? = null, message: String? = null,
override val cause: Throwable? = null cause: Throwable? = null
) : IOException( ) : BotException(
message ?: "Something went wrong" message ?: "Something went wrong",
cause
) )
class CommonRequestException(response: Response, plainAnswer: String, message: String?, cause: Throwable?) : class CommonRequestException(response: Response, plainAnswer: String, message: String?, cause: Throwable?) :

View File

@ -1,9 +1,10 @@
package dev.inmo.tgbotapi.bot.ktor package dev.inmo.tgbotapi.bot.ktor
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.safely import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.tgbotapi.bot.BaseRequestsExecutor import dev.inmo.tgbotapi.bot.BaseRequestsExecutor
import dev.inmo.tgbotapi.bot.TelegramBot 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.ktor.base.*
import dev.inmo.tgbotapi.bot.settings.limiters.ExceptionsOnlyLimiter import dev.inmo.tgbotapi.bot.settings.limiters.ExceptionsOnlyLimiter
import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter
@ -48,12 +49,36 @@ class KtorRequestsExecutor(
} }
override suspend fun <T : Any> execute(request: Request<T>): T { override suspend fun <T : Any> execute(request: Request<T>): T {
return runCatching { return runCatchingSafely {
safely( pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories)
{ e -> requestsLimiter.limit {
pipelineStepsHolder.onRequestException(request, e) ?.let { return@safely it } 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 content = e.response.bodyAsText()
val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content) val responseObject = jsonFormatter.decodeFromString(Response.serializer(), content)
newRequestException( newRequestException(
@ -61,38 +86,15 @@ class KtorRequestsExecutor(
content, content,
"Can't get result object from $content" "Can't get result object from $content"
) )
} else {
e
} }
exceptionResult.exceptionOrNull() ?.let {
CommonBotException(cause = e)
} ?: exceptionResult.getOrThrow()
} else {
CommonBotException(cause = e)
} }
) { } ?.let { Result.failure(it) } ?: it
pipelineStepsHolder.onBeforeSearchCallFactory(request, callsFactories) pipelineStepsHolder.onRequestReturnResult(result, 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)
} }
} }

View File

@ -19,7 +19,7 @@ class MultipartSetWebhookRequest(
certificate: MultipartFile, certificate: MultipartFile,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) : SetWebhookRequest(), MultipartRequest<Boolean> by MultipartRequestImpl( ) : SetWebhookRequest(), MultipartRequest<Boolean> by MultipartRequestImpl(
@ -40,7 +40,7 @@ fun SetWebhook(
certificate: MultipartFile, certificate: MultipartFile,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
): MultipartSetWebhookRequest = MultipartSetWebhookRequest( ): MultipartSetWebhookRequest = MultipartSetWebhookRequest(
@ -58,7 +58,7 @@ fun SetWebhook(
certificate: FileId, certificate: FileId,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
): SetWebhook = SetWebhook( ): SetWebhook = SetWebhook(
@ -84,7 +84,7 @@ fun SetWebhook(
certificate: InputFile, certificate: InputFile,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) = when (certificate) { ) = when (certificate) {
@ -104,7 +104,7 @@ fun SetWebhook(
url: String, url: String,
ipAddress: String? = null, ipAddress: String? = null,
maxAllowedConnections: Int? = null, maxAllowedConnections: Int? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
dropPendingUpdates: Boolean? = null, dropPendingUpdates: Boolean? = null,
secretToken: String? = null secretToken: String? = null
) = SetWebhook( ) = SetWebhook(
@ -135,7 +135,7 @@ data class SetWebhook internal constructor(
@SerialName(maxAllowedConnectionsField) @SerialName(maxAllowedConnectionsField)
val maxAllowedConnections: Int? = null, val maxAllowedConnections: Int? = null,
@SerialName(allowedUpdatesField) @SerialName(allowedUpdatesField)
val allowedUpdates: List<String>? = null, val allowedUpdates: List<String>? = ALL_UPDATES_LIST,
@SerialName(dropPendingUpdatesField) @SerialName(dropPendingUpdatesField)
val dropPendingUpdates: Boolean? = null, val dropPendingUpdates: Boolean? = null,
@SerialName(secretTokenField) @SerialName(secretTokenField)

View File

@ -81,6 +81,8 @@ sealed interface StickerType {
} }
} }
val usernameRegex = Regex("@[\\w\\d_]+")
val degreesLimit = 1 .. 360 val degreesLimit = 1 .. 360
val horizontalAccuracyLimit = 0F .. 1500F val horizontalAccuracyLimit = 0F .. 1500F

View File

@ -1,5 +1,7 @@
package dev.inmo.tgbotapi.types.message.textsources 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.RiskFeature
import dev.inmo.tgbotapi.utils.internal.* import dev.inmo.tgbotapi.utils.internal.*
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -16,6 +18,9 @@ data class BotCommandTextSource @RiskFeature(DirectInvocationOfTextSourceConstru
val command: String by lazy { val command: String by lazy {
commandRegex.find(source) ?.value ?.substring(1) ?: source.substring(1)// skip first symbol like "/" or "!" 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 markdown: String by lazy { source.commandMarkdown() }
override val markdownV2: String by lazy { source.commandMarkdownV2() } override val markdownV2: String by lazy { source.commandMarkdownV2() }

View File

@ -30,7 +30,7 @@ fun <T> Flow<Iterable<T>>.flatten(): Flow<T> = flow {
} }
} }
fun <T, R> Flow<T>.flatMap(mapper: (T) -> Iterable<R>): Flow<R> = flow { fun <T, R> Flow<T>.flatMap(mapper: suspend (T) -> Iterable<R>): Flow<R> = flow {
collect { collect {
mapper(it).forEach { mapper(it).forEach {
emit(it) emit(it)

View File

@ -4,6 +4,7 @@ import dev.inmo.tgbotapi.types.*
import dev.inmo.tgbotapi.types.chat.* import dev.inmo.tgbotapi.types.chat.*
import dev.inmo.tgbotapi.types.message.abstracts.Message import dev.inmo.tgbotapi.types.message.abstracts.Message
import dev.inmo.tgbotapi.types.message.textsources.link import dev.inmo.tgbotapi.types.message.textsources.link
import io.ktor.http.encodeURLQueryComponent
fun makeUsernameLink(username: String) = "$internalLinkBeginning/$username" fun makeUsernameLink(username: String) = "$internalLinkBeginning/$username"
@ -17,11 +18,11 @@ inline val Username.deepLinkPrefix
inline val Username.startattachPrefix inline val Username.startattachPrefix
get() = makeUsernameStartattachPrefix(usernameWithoutAt) get() = makeUsernameStartattachPrefix(usernameWithoutAt)
inline fun makeLink(username: Username) = username.link 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 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 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( fun makeLinkToMessage(
username: String, username: String,

View File

@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.*
fun TelegramBot.longPollingFlow( fun TelegramBot.longPollingFlow(
timeoutSeconds: Seconds = 30, timeoutSeconds: Seconds = 30,
exceptionsHandler: (ExceptionHandler<Unit>)? = null, exceptionsHandler: (ExceptionHandler<Unit>)? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
): Flow<Update> = channelFlow { ): Flow<Update> = channelFlow {
var lastUpdateIdentifier: UpdateIdentifier? = null var lastUpdateIdentifier: UpdateIdentifier? = null
@ -81,7 +81,7 @@ fun TelegramBot.startGettingOfUpdatesByLongPolling(
timeoutSeconds: Seconds = 30, timeoutSeconds: Seconds = 30,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
exceptionsHandler: (ExceptionHandler<Unit>)? = null, exceptionsHandler: (ExceptionHandler<Unit>)? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
updatesReceiver: UpdateReceiver<Update> updatesReceiver: UpdateReceiver<Update>
): Job = longPollingFlow(timeoutSeconds, exceptionsHandler, allowedUpdates).subscribeSafely( ): Job = longPollingFlow(timeoutSeconds, exceptionsHandler, allowedUpdates).subscribeSafely(
scope, scope,
@ -97,7 +97,7 @@ fun TelegramBot.createAccumulatedUpdatesRetrieverFlow(
avoidInlineQueries: Boolean = false, avoidInlineQueries: Boolean = false,
avoidCallbackQueries: Boolean = false, avoidCallbackQueries: Boolean = false,
exceptionsHandler: ExceptionHandler<Unit>? = null, exceptionsHandler: ExceptionHandler<Unit>? = null,
allowedUpdates: List<String>? = null allowedUpdates: List<String>? = ALL_UPDATES_LIST
): Flow<Update> = longPollingFlow( ): Flow<Update> = longPollingFlow(
timeoutSeconds = 0, timeoutSeconds = 0,
exceptionsHandler = { exceptionsHandler = {
@ -117,7 +117,7 @@ fun TelegramBot.retrieveAccumulatedUpdates(
avoidCallbackQueries: Boolean = false, avoidCallbackQueries: Boolean = false,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
exceptionsHandler: (ExceptionHandler<Unit>)? = null, exceptionsHandler: (ExceptionHandler<Unit>)? = null,
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
updatesReceiver: UpdateReceiver<Update> updatesReceiver: UpdateReceiver<Update>
): Job = createAccumulatedUpdatesRetrieverFlow( ): Job = createAccumulatedUpdatesRetrieverFlow(
avoidInlineQueries, avoidInlineQueries,
@ -149,7 +149,7 @@ suspend fun TelegramBot.flushAccumulatedUpdates(
avoidInlineQueries: Boolean = false, avoidInlineQueries: Boolean = false,
avoidCallbackQueries: Boolean = false, avoidCallbackQueries: Boolean = false,
scope: CoroutineScope = CoroutineScope(Dispatchers.Default), scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
allowedUpdates: List<String>? = null, allowedUpdates: List<String>? = ALL_UPDATES_LIST,
exceptionsHandler: ExceptionHandler<Unit>? = null, exceptionsHandler: ExceptionHandler<Unit>? = null,
updatesReceiver: UpdateReceiver<Update> = {} updatesReceiver: UpdateReceiver<Update> = {}
) = retrieveAccumulatedUpdates( ) = retrieveAccumulatedUpdates(