diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ae73b28d..4b534b401a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 18.2.3 +* `Core`: + * Add default middleware `ExceptionsThrottlerTelegramBotMiddleware` + * Make `TelegramBotMiddlewaresPipelinesHandler` to be default `TelegramBotPipelinesHandler` + ## 18.2.2 * `Version`: 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 ddba388517..c23d52d6af 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 @@ -2,6 +2,7 @@ package dev.inmo.tgbotapi.bot.ktor import dev.inmo.kslog.common.KSLog import dev.inmo.tgbotapi.bot.BaseRequestsExecutor +import dev.inmo.tgbotapi.bot.ktor.middlewares.TelegramBotMiddlewaresPipelinesHandler import dev.inmo.tgbotapi.bot.settings.limiters.ExceptionsOnlyLimiter import dev.inmo.tgbotapi.bot.settings.limiters.RequestLimiter import dev.inmo.tgbotapi.requests.abstracts.Request @@ -39,7 +40,7 @@ fun KtorRequestsExecutor( excludeDefaultFactories: Boolean = false, requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter, jsonFormatter: Json = nonstrictJsonFormat, - pipelineStepsHolder: TelegramBotPipelinesHandler = TelegramBotPipelinesHandler, + pipelineStepsHolder: TelegramBotPipelinesHandler = TelegramBotMiddlewaresPipelinesHandler(), logger: KSLog = DefaultKTgBotAPIKSLog, ) = KtorRequestsExecutor( telegramAPIUrlsKeeper = telegramAPIUrlsKeeper, diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutorFactories.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutorFactories.kt index 8b46427622..768d99fcb6 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutorFactories.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/KtorRequestsExecutorFactories.kt @@ -27,7 +27,7 @@ class KtorRequestsExecutorBuilder( var requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter var jsonFormatter: Json = nonstrictJsonFormat var logger: KSLog = DefaultKTgBotAPIKSLog - var pipelineStepsHolder: TelegramBotPipelinesHandler = TelegramBotPipelinesHandler + var pipelineStepsHolder: TelegramBotPipelinesHandler = TelegramBotMiddlewaresPipelinesHandler() fun includeMiddlewares(block: TelegramBotMiddlewaresPipelinesHandler.Builder.() -> Unit) { pipelineStepsHolder = TelegramBotMiddlewaresPipelinesHandler.build(block) diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddleware.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddleware.kt index 981742d6c7..719217843f 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddleware.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddleware.kt @@ -1,5 +1,6 @@ package dev.inmo.tgbotapi.bot.ktor.middlewares +import com.benasher44.uuid.uuid4 import dev.inmo.micro_utils.common.Warning import dev.inmo.tgbotapi.bot.ktor.KtorCallFactory import dev.inmo.tgbotapi.bot.ktor.TelegramBotPipelinesHandler @@ -22,7 +23,7 @@ import dev.inmo.tgbotapi.requests.abstracts.Request * Non-null result of lambda will be used as the result of request handling */ @Warning("This API is experimental and subject of changes") -class TelegramBotMiddleware( +open class TelegramBotMiddleware( internal val onRequestException: (suspend (request: Request<*>, t: Throwable?) -> Any?)? = null, internal val onBeforeSearchCallFactory: (suspend (request: Request<*>, callsFactories: List) -> Unit)? = null, internal val onBeforeCallFactoryMakeCall: (suspend (request: Request<*>, potentialFactory: KtorCallFactory) -> Unit)? = null, @@ -30,6 +31,7 @@ class TelegramBotMiddleware( internal val onRequestResultPresented: (suspend (result: Any?, request: Request<*>, resultCallFactory: KtorCallFactory, callsFactories: List) -> Any?)? = null, internal val onRequestResultAbsent: (suspend (request: Request<*>, callsFactories: List) -> Any?)? = null, internal val onRequestReturnResult: (suspend (result: Result<*>, request: Request<*>, callsFactories: List) -> Result?)? = null, + val id: String = uuid4().toString() ) : TelegramBotPipelinesHandler { object ResultAbsence : Throwable() override suspend fun onRequestException(request: Request, t: Throwable): T? { diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddlewareBuilder.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddlewareBuilder.kt index 94c9bdd875..6400fc7c3b 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddlewareBuilder.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddlewareBuilder.kt @@ -1,5 +1,6 @@ package dev.inmo.tgbotapi.bot.ktor.middlewares +import com.benasher44.uuid.uuid4 import dev.inmo.micro_utils.common.Warning import dev.inmo.tgbotapi.bot.ktor.KtorCallFactory import dev.inmo.tgbotapi.bot.ktor.TelegramBotPipelinesHandler @@ -14,6 +15,7 @@ class TelegramBotMiddlewareBuilder { var onRequestResultPresented: (suspend (result: Any?, request: Request<*>, resultCallFactory: KtorCallFactory, callsFactories: List) -> Any?)? = null var onRequestResultAbsent: (suspend (request: Request<*>, callsFactories: List) -> Any?)? = null var onRequestReturnResult: (suspend (result: Result<*>, request: Request<*>, callsFactories: List) -> Result?)? = null + var id: String = uuid4().toString() /** * Useful way to set [onRequestException] @@ -67,7 +69,8 @@ class TelegramBotMiddlewareBuilder { onAfterCallFactoryMakeCall = onAfterCallFactoryMakeCall, onRequestResultPresented = onRequestResultPresented, onRequestResultAbsent = onRequestResultAbsent, - onRequestReturnResult = onRequestReturnResult + onRequestReturnResult = onRequestReturnResult, + id = id ) } @@ -82,6 +85,7 @@ class TelegramBotMiddlewareBuilder { onRequestResultPresented = middleware.onRequestResultPresented onRequestResultAbsent = middleware.onRequestResultAbsent onRequestReturnResult = middleware.onRequestReturnResult + id = middleware.id additionalSetup() }.build() } diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddlewaresPipelinesHandler.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddlewaresPipelinesHandler.kt index fbcc976d2f..9f868db65a 100644 --- a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddlewaresPipelinesHandler.kt +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/TelegramBotMiddlewaresPipelinesHandler.kt @@ -3,11 +3,12 @@ package dev.inmo.tgbotapi.bot.ktor.middlewares import dev.inmo.micro_utils.common.Warning import dev.inmo.tgbotapi.bot.ktor.KtorCallFactory import dev.inmo.tgbotapi.bot.ktor.TelegramBotPipelinesHandler +import dev.inmo.tgbotapi.bot.ktor.middlewares.builtins.ExceptionsThrottlerTelegramBotMiddleware import dev.inmo.tgbotapi.requests.abstracts.Request @Warning("This API is experimental and subject of changes") class TelegramBotMiddlewaresPipelinesHandler( - private val middlewares: List + private val middlewares: List = listOf(ExceptionsThrottlerTelegramBotMiddleware()) ) : TelegramBotPipelinesHandler { override suspend fun onRequestException(request: Request, t: Throwable): T? { return middlewares.firstNotNullOfOrNull { @@ -72,6 +73,7 @@ class TelegramBotMiddlewaresPipelinesHandler( @Warning("This API is experimental and subject of changes") class Builder { + @Warning("This API is experimental and subject of changes") val middlewares = mutableListOf() @Warning("This API is experimental and subject of changes") diff --git a/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/builtins/ExceptionsThrottlerTelegramBotMiddleware.kt b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/builtins/ExceptionsThrottlerTelegramBotMiddleware.kt new file mode 100644 index 0000000000..e450912bfb --- /dev/null +++ b/tgbotapi.core/src/commonMain/kotlin/dev/inmo/tgbotapi/bot/ktor/middlewares/builtins/ExceptionsThrottlerTelegramBotMiddleware.kt @@ -0,0 +1,59 @@ +package dev.inmo.tgbotapi.bot.ktor.middlewares.builtins + +import dev.inmo.tgbotapi.bot.ktor.middlewares.TelegramBotMiddleware +import dev.inmo.tgbotapi.requests.abstracts.Request +import korlibs.time.milliseconds +import kotlinx.coroutines.delay +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlin.reflect.KClass +import kotlin.time.Duration + +/** + * @see invoke + */ +object ExceptionsThrottlerTelegramBotMiddleware { + const val id: String = "ExceptionsThrottlerTelegramBotMiddleware" + + /** + * Creates [TelegramBotMiddleware] and configures it with next parameters: + * + * * [TelegramBotMiddleware.onRequestException] will throttle after exception if exception has happened before + * * [TelegramBotMiddleware.onRequestReturnResult] will clear state of all exceptions happened with the [Request] if its + * handling has been completed successfully + */ + operator fun invoke( + exceptionDurationMultiplier: Float = 2f, + initialExceptionDuration: Duration = 125.milliseconds, + ): TelegramBotMiddleware = TelegramBotMiddleware.build { + val exceptionsTimeouts = mutableMapOf, Duration>() + val latestExceptionsRequestsTypes = mutableMapOf, MutableSet>>() + val mutex = Mutex() + onRequestException = onRequestException@{ request, t -> + t ?: return@onRequestException null + val kclass = t::class + val toSleep = mutex.withLock { + val latestDuration = exceptionsTimeouts[kclass] + exceptionsTimeouts[kclass] = latestDuration ?.times(exceptionDurationMultiplier.toDouble()) ?: initialExceptionDuration + latestExceptionsRequestsTypes.getOrPut(request::class) { mutableSetOf() }.add(kclass) + latestDuration + } + toSleep ?.let { + delay(it) + } + null + } + onRequestReturnResult = onRequestReturnResult@{ result, request, _ -> + if (result.isSuccess) { + mutex.withLock { + val exceptionKClass = latestExceptionsRequestsTypes.remove(request::class) ?: return@withLock + exceptionKClass.forEach { + exceptionsTimeouts.remove(it) + } + } + } + null + } + id = ExceptionsThrottlerTelegramBotMiddleware.id + } +}