Merge pull request #217 from InsanusMokrassar/0.30.7

0.30.7
This commit is contained in:
InsanusMokrassar 2020-11-17 16:32:19 +06:00 committed by GitHub
commit 1ce2526401
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 131 additions and 95 deletions

View File

@ -1,5 +1,19 @@
# TelegramBotAPI changelog # TelegramBotAPI changelog
## 0.30.7
* `Common`:
* `Version`:
* `MicroUtils`: `0.4.0` -> `0.4.1`
* `Core`:
* `TelegramAPIUrlsKeeper` will fix ending of host url since this version
* New mechanisms in`PowLimiter` and `CommonLimiter` has been added
* New builder `KtorRequestsExecutorBuilder`
* New function `telegramBot`
* `Utils`:
* Simple function `telegramBot(TelegramAPIUrlsKeeper)` has been deprecated with replacement by almost the same
function in `Core`
## 0.30.6 ## 0.30.6
* `Core` * `Core`

View File

@ -12,12 +12,12 @@ klock_version=1.12.1
uuid_version=0.2.2 uuid_version=0.2.2
ktor_version=1.4.2 ktor_version=1.4.2
micro_utils_version=0.4.0 micro_utils_version=0.4.1
javax_activation_version=1.1.1 javax_activation_version=1.1.1
library_group=dev.inmo library_group=dev.inmo
library_version=0.30.6 library_version=0.30.7
gradle_bintray_plugin_version=1.8.5 gradle_bintray_plugin_version=1.8.5
github_release_plugin_version=2.2.12 github_release_plugin_version=2.2.12

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip

View File

@ -3,6 +3,7 @@ package dev.inmo.tgbotapi.bot.Ktor
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.Ktor.base.* import dev.inmo.tgbotapi.bot.Ktor.base.*
import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.bot.exceptions.newRequestException import dev.inmo.tgbotapi.bot.exceptions.newRequestException
import dev.inmo.tgbotapi.bot.settings.limiters.* import dev.inmo.tgbotapi.bot.settings.limiters.*
import dev.inmo.tgbotapi.requests.abstracts.Request import dev.inmo.tgbotapi.requests.abstracts.Request
@ -13,6 +14,33 @@ import io.ktor.client.features.*
import io.ktor.client.statement.readText import io.ktor.client.statement.readText
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class KtorRequestsExecutorBuilder(
var telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper
) {
var client: HttpClient = HttpClient()
var callsFactories: List<KtorCallFactory> = emptyList()
var excludeDefaultFactories: Boolean = false
var requestsLimiter: RequestLimiter = ExceptionsOnlyLimiter()
var jsonFormatter: Json = nonstrictJsonFormat
fun build() = KtorRequestsExecutor(telegramAPIUrlsKeeper, client, callsFactories, excludeDefaultFactories, requestsLimiter, jsonFormatter)
}
inline fun telegramBot(
telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper,
crossinline builder: KtorRequestsExecutorBuilder.() -> Unit = {}
): TelegramBot = KtorRequestsExecutorBuilder(telegramAPIUrlsKeeper).apply(builder).build()
/**
* Shortcut for [telegramBot]
*/
@Suppress("NOTHING_TO_INLINE")
inline fun telegramBot(
token: String,
apiUrl: String = telegramBotAPIDefaultUrl,
crossinline builder: KtorRequestsExecutorBuilder.() -> Unit = {}
): TelegramBot = telegramBot(TelegramAPIUrlsKeeper(token, apiUrl), builder)
class KtorRequestsExecutor( class KtorRequestsExecutor(
telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper, telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper,
client: HttpClient = HttpClient(), client: HttpClient = HttpClient(),

View File

@ -1,67 +1,43 @@
package dev.inmo.tgbotapi.bot.settings.limiters package dev.inmo.tgbotapi.bot.settings.limiters
import com.soywiz.klock.DateTime import com.soywiz.klock.DateTime
import dev.inmo.micro_utils.coroutines.*
import dev.inmo.tgbotapi.types.MilliSeconds
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.sync.Semaphore
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlin.coroutines.Continuation
import kotlin.math.roundToLong
private fun now(): Long = DateTime.nowUnixLong() private fun now(): Long = DateTime.nowUnixLong()
@Serializable
class CommonLimiter( class CommonLimiter(
private val lockCount: Int = 10, private val lockCount: Int = 10,
private val regenTime: Long = 20 * 1000L // 20 seconds for full regen of opportunity to send message private val regenTime: MilliSeconds = 15 * 1000, // 15 seconds for full regen of opportunity to send message
@Transient
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : RequestLimiter { ) : RequestLimiter {
private var doLimit: Boolean = false private val quotaSemaphore = Semaphore(lockCount)
private val counterRegeneratorJob = scope.launch {
private val counterChannel = Channel<Unit>(Channel.UNLIMITED) val regenDelay: MilliSeconds = (regenTime.toDouble() / lockCount).roundToLong()
private val scope = CoroutineScope(Dispatchers.Default) while (isActive) {
private val counterJob = scope.launch { delay(regenDelay)
var wasLastSecond = 0 if (quotaSemaphore.availablePermits < lockCount) {
var lastCountTime = now() try {
var limitManagementJob: Job? = null quotaSemaphore.release()
var removeLimitTime: Long = lastCountTime } catch (_: IllegalStateException) {
for (counter in counterChannel) { // Skip IllegalStateException due to the fact that this exception may happens in release method
val now = now()
if (now - lastCountTime > 1000) {
lastCountTime = now
wasLastSecond = 1
} else {
wasLastSecond++
}
if (wasLastSecond >= lockCount) {
removeLimitTime = now + regenTime
if (limitManagementJob == null) {
limitManagementJob = launch {
doLimit = true
var internalNow = now()
while (internalNow < removeLimitTime) {
delay(removeLimitTime - internalNow)
internalNow = now()
}
doLimit = false
}
} }
} }
if (now > removeLimitTime) {
limitManagementJob = null
}
}
}
private val quoterChannel = Channel<Unit>(Channel.CONFLATED)
private val tickerJob = scope.launch {
while (isActive) {
quoterChannel.send(Unit)
delay(1000L)
} }
} }
override suspend fun <T> limit(block: suspend () -> T): T { override suspend fun <T> limit(block: suspend () -> T): T {
counterChannel.send(Unit) quotaSemaphore.acquire()
return if (!doLimit) { return block()
block()
} else {
quoterChannel.receive()
block()
}
} }
} }

View File

@ -1,5 +1,7 @@
package dev.inmo.tgbotapi.bot.settings.limiters package dev.inmo.tgbotapi.bot.settings.limiters
import dev.inmo.micro_utils.coroutines.*
import dev.inmo.tgbotapi.types.MilliSeconds
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -9,62 +11,60 @@ import kotlin.math.pow
private sealed class RequestEvent private sealed class RequestEvent
private class AddRequest( private class AddRequest(
val continuation: Continuation<Long> val continuation: Continuation<MilliSeconds>
) : RequestEvent() ) : RequestEvent()
private object CompleteRequest : RequestEvent() private object CompleteRequest : RequestEvent()
@Serializable @Serializable
data class PowLimiter( data class PowLimiter(
private val minAwaitTime: Long = 0L, private val minAwaitTime: MilliSeconds = 0L,
private val maxAwaitTime: Long = 10000L, private val maxAwaitTime: MilliSeconds = 10000L,
private val powValue: Double = 4.0, private val powValue: Double = 4.0,
private val powK: Double = 0.0016 private val powK: Double = 1.6,
@Transient
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default)
) : RequestLimiter { ) : RequestLimiter {
@Transient
private val scope = CoroutineScope(Dispatchers.Default)
@Transient
private val eventsChannel = Channel<RequestEvent>(Channel.UNLIMITED)
@Transient @Transient
private val awaitTimeRange = minAwaitTime .. maxAwaitTime private val awaitTimeRange = minAwaitTime .. maxAwaitTime
@Transient
private val eventsChannel = let {
var requestsInWork = 0.0
scope.actor<RequestEvent> {
when (it) {
is AddRequest -> {
val awaitTime = (requestsInWork.pow(powValue) * powK).toLong()
requestsInWork++
init { it.continuation.resume(
scope.launch { when {
var requestsInWork: Double = 0.0 awaitTime in awaitTimeRange -> awaitTime
for (event in eventsChannel) { awaitTime < awaitTimeRange.first -> awaitTimeRange.first
when (event) { else -> awaitTimeRange.last
is AddRequest -> { }
val awaitTime = (((requestsInWork.pow(powValue) * powK) * 1000L).toLong()) )
requestsInWork++
event.continuation.resume(
if (awaitTime in awaitTimeRange) {
awaitTime
} else {
if (awaitTime < minAwaitTime) {
minAwaitTime
} else {
maxAwaitTime
}
}
)
}
is CompleteRequest -> requestsInWork--
} }
is CompleteRequest -> requestsInWork--
} }
} }
} }
override suspend fun <T> limit( private suspend inline fun <T> withDelay(
block: suspend () -> T crossinline block: suspend () -> T
): T { ): T {
val delayMillis = suspendCoroutine<Long> { val delayMillis = suspendCoroutine<Long> {
scope.launch { eventsChannel.send(AddRequest(it)) } scope.launch { eventsChannel.send(AddRequest(it)) }
} }
delay(delayMillis) delay(delayMillis)
return try { return try {
block() safely { block() }
} finally { } finally {
eventsChannel.send(CompleteRequest) eventsChannel.send(CompleteRequest)
} }
} }
override suspend fun <T> limit(
block: suspend () -> T
): T {
return withDelay(block)
}
} }

View File

@ -2,10 +2,28 @@ package dev.inmo.tgbotapi.utils
const val telegramBotAPIDefaultUrl = "https://api.telegram.org" const val telegramBotAPIDefaultUrl = "https://api.telegram.org"
private inline val String.withoutLastSlash: String
get() {
var correctedUrl = this
while (true) {
val withoutSuffix = correctedUrl.removeSuffix("/")
if (withoutSuffix == correctedUrl) {
return correctedUrl
}
correctedUrl = withoutSuffix
}
}
class TelegramAPIUrlsKeeper( class TelegramAPIUrlsKeeper(
token: String, token: String,
hostUrl: String = telegramBotAPIDefaultUrl hostUrl: String = telegramBotAPIDefaultUrl
) { ) {
val commonAPIUrl = "$hostUrl/bot$token" val commonAPIUrl: String
val fileBaseUrl = "$hostUrl/file/bot$token" val fileBaseUrl: String
init {
val correctedHost = hostUrl.withoutLastSlash
commonAPIUrl = "$correctedHost/bot$token"
fileBaseUrl = "$correctedHost/file/bot$token"
}
} }

View File

@ -1,6 +1,6 @@
package dev.inmo.tgbotapi.extensions.api package dev.inmo.tgbotapi.extensions.api
import dev.inmo.tgbotapi.bot.Ktor.KtorRequestsExecutor import dev.inmo.tgbotapi.bot.Ktor.KtorRequestsExecutorBuilder
import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.bot.TelegramBot
import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper import dev.inmo.tgbotapi.utils.TelegramAPIUrlsKeeper
import dev.inmo.tgbotapi.utils.telegramBotAPIDefaultUrl import dev.inmo.tgbotapi.utils.telegramBotAPIDefaultUrl
@ -11,11 +11,11 @@ import io.ktor.client.engine.*
/** /**
* Allows to create bot using bot [urlsKeeper] * Allows to create bot using bot [urlsKeeper]
*/ */
@Deprecated("Replaced in core", ReplaceWith("telegramBot", "dev.inmo.tgbotapi.bot.Ktor.telegramBot"))
fun telegramBot( fun telegramBot(
urlsKeeper: TelegramAPIUrlsKeeper urlsKeeper: TelegramAPIUrlsKeeper
): TelegramBot = KtorRequestsExecutor( ): TelegramBot = dev.inmo.tgbotapi.bot.Ktor.telegramBot(
urlsKeeper, urlsKeeper
HttpClient()
) )
/** /**
@ -24,10 +24,9 @@ fun telegramBot(
fun telegramBot( fun telegramBot(
urlsKeeper: TelegramAPIUrlsKeeper, urlsKeeper: TelegramAPIUrlsKeeper,
client: HttpClient client: HttpClient
): TelegramBot = KtorRequestsExecutor( ): TelegramBot = dev.inmo.tgbotapi.bot.Ktor.telegramBot(urlsKeeper) {
urlsKeeper, this.client = client
client }
)
/** /**
* Allows to create bot using bot [urlsKeeper] and specify [HttpClientEngineFactory] by passing [clientFactory] param and optionally * Allows to create bot using bot [urlsKeeper] and specify [HttpClientEngineFactory] by passing [clientFactory] param and optionally
@ -73,11 +72,12 @@ inline fun telegramBot(
/** /**
* Allows to create bot using bot [token], [apiUrl] (for custom api servers) and already prepared [client] * Allows to create bot using bot [token], [apiUrl] (for custom api servers) and already prepared [client]
*/ */
@Deprecated("Replaced in core", ReplaceWith("telegramBot", "dev.inmo.tgbotapi.bot.Ktor.telegramBot"))
@Suppress("NOTHING_TO_INLINE") @Suppress("NOTHING_TO_INLINE")
inline fun telegramBot( inline fun telegramBot(
token: String, token: String,
apiUrl: String = telegramBotAPIDefaultUrl apiUrl: String = telegramBotAPIDefaultUrl
): TelegramBot = telegramBot(TelegramAPIUrlsKeeper(token, apiUrl)) ): TelegramBot = dev.inmo.tgbotapi.bot.Ktor.telegramBot(token, apiUrl)
/** /**
* Allows to create bot using bot [token], [apiUrl] (for custom api servers) and already prepared [client] * Allows to create bot using bot [token], [apiUrl] (for custom api servers) and already prepared [client]