1
0
mirror of https://github.com/InsanusMokrassar/TelegramBotAPI.git synced 2025-11-15 03:20:18 +00:00

migration

This commit is contained in:
2019-12-03 11:07:25 +06:00
parent 24d11d2c2b
commit f3fc0769ef
449 changed files with 265 additions and 178 deletions

View File

@@ -0,0 +1,7 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot
import com.github.insanusmokrassar.TelegramBotAPI.utils.TelegramAPIUrlsKeeper
abstract class BaseRequestsExecutor(
protected val telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper
) : RequestsExecutor

View File

@@ -0,0 +1,13 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.Request
import io.ktor.client.HttpClient
import io.ktor.client.call.HttpClientCall
interface KtorCallFactory {
suspend fun <T: Any> prepareCall(
client: HttpClient,
baseUrl: String,
request: Request<T>
) : HttpClientCall?
}

View File

@@ -0,0 +1,77 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor
import com.github.insanusmokrassar.TelegramBotAPI.bot.BaseRequestsExecutor
import com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.base.MultipartRequestCallFactory
import com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.base.SimpleRequestCallFactory
import com.github.insanusmokrassar.TelegramBotAPI.bot.exceptions.newRequestException
import com.github.insanusmokrassar.TelegramBotAPI.bot.settings.limiters.EmptyLimiter
import com.github.insanusmokrassar.TelegramBotAPI.bot.settings.limiters.RequestLimiter
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.Request
import com.github.insanusmokrassar.TelegramBotAPI.types.Response
import com.github.insanusmokrassar.TelegramBotAPI.types.RetryAfterError
import com.github.insanusmokrassar.TelegramBotAPI.utils.TelegramAPIUrlsKeeper
import io.ktor.client.HttpClient
import io.ktor.client.call.HttpClientCall
import io.ktor.client.call.receive
import kotlinx.coroutines.delay
import kotlinx.serialization.json.Json
class KtorRequestsExecutor(
telegramAPIUrlsKeeper: TelegramAPIUrlsKeeper,
private val client: HttpClient = HttpClient(),
callsFactories: List<KtorCallFactory> = emptyList(),
excludeDefaultFactories: Boolean = false,
private val requestsLimiter: RequestLimiter = EmptyLimiter,
private val jsonFormatter: Json = Json.nonstrict
) : BaseRequestsExecutor(telegramAPIUrlsKeeper) {
private val callsFactories: List<KtorCallFactory> = callsFactories.run {
if (!excludeDefaultFactories) {
asSequence().plus(SimpleRequestCallFactory()).plus(MultipartRequestCallFactory()).toList()
} else {
this
}
}
override suspend fun <T : Any> execute(request: Request<T>): T {
return requestsLimiter.limit {
var call: HttpClientCall? = null
for (factory in callsFactories) {
call = factory.prepareCall(
client,
telegramAPIUrlsKeeper.commonAPIUrl,
request
)
if (call != null) {
break
}
}
if (call == null) {
throw IllegalArgumentException("Can't execute request: $request")
}
val content = call.response.receive<String>()
val responseObject = jsonFormatter.parse(Response.serializer(), content)
(responseObject.result ?.let {
jsonFormatter.fromJson(request.resultDeserializer, it)
} ?: responseObject.parameters ?.let {
val error = it.error
if (error is RetryAfterError) {
delay(error.leftToRetry)
execute(request)
} else {
null
}
} ?: call.let {
throw newRequestException(
responseObject,
content,
"Can't get result object from $content"
)
})
}
}
override fun close() {
client.close()
}
}

View File

@@ -0,0 +1,40 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.base
import com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.KtorCallFactory
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.Request
import io.ktor.client.HttpClient
import io.ktor.client.call.HttpClientCall
import io.ktor.client.call.call
import io.ktor.client.request.accept
import io.ktor.client.request.url
import io.ktor.http.ContentType
import io.ktor.http.HttpMethod
abstract class AbstractRequestCallFactory : KtorCallFactory {
private val methodsCache: MutableMap<String, String> = mutableMapOf()
override suspend fun <T : Any> prepareCall(
client: HttpClient,
baseUrl: String,
request: Request<T>
): HttpClientCall? {
val preparedBody = prepareCallBody(client, baseUrl, request) ?: return null
return client.call {
url(
methodsCache[request.method()] ?: "$baseUrl/${request.method()}".also {
methodsCache[request.method()] = it
}
)
method = HttpMethod.Post
accept(ContentType.Application.Json)
body = preparedBody
}
}
protected abstract fun <T : Any> prepareCallBody(
client: HttpClient,
baseUrl: String,
request: Request<T>
): Any?
}

View File

@@ -0,0 +1,39 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.base
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.*
import com.github.insanusmokrassar.TelegramBotAPI.utils.mapWithCommonValues
import io.ktor.client.HttpClient
import io.ktor.client.request.forms.MultiPartFormDataContent
import io.ktor.client.request.forms.formData
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import kotlinx.io.core.readBytes
class MultipartRequestCallFactory : AbstractRequestCallFactory() {
override fun <T : Any> prepareCallBody(
client: HttpClient,
baseUrl: String,
request: Request<T>
): Any? = (request as? MultipartRequest) ?.let { castedRequest ->
MultiPartFormDataContent(
formData {
val params = castedRequest.paramsJson.mapWithCommonValues()
for ((key, value) in castedRequest.mediaMap + params) {
when (value) {
is MultipartFile -> append(
key,
value.file.asInput().readBytes(),
Headers.build {
append(HttpHeaders.ContentType, value.mimeType)
append(HttpHeaders.ContentDisposition, "filename=${value.fileId}")
}
)
is FileId -> append(key, value.fileId)
else -> append(key, value.toString())
}
}
}
)
}
}

View File

@@ -0,0 +1,22 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.Ktor.base
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.*
import com.github.insanusmokrassar.TelegramBotAPI.utils.toJsonWithoutNulls
import io.ktor.client.HttpClient
import io.ktor.http.ContentType
import io.ktor.http.content.TextContent
class SimpleRequestCallFactory : AbstractRequestCallFactory() {
override fun <T : Any> prepareCallBody(
client: HttpClient,
baseUrl: String,
request: Request<T>
): Any? = (request as? SimpleRequest<T>) ?.let { _ ->
val content = request.json().toString()
TextContent(
content,
ContentType.Application.Json
)
}
}

View File

@@ -0,0 +1,11 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot
import com.github.insanusmokrassar.TelegramBotAPI.requests.abstracts.Request
import kotlinx.io.core.Closeable
interface RequestsExecutor : Closeable {
/**
* @throws com.github.insanusmokrassar.TelegramBotAPI.bot.exceptions.RequestException
*/
suspend fun <T : Any> execute(request: Request<T>): T
}

View File

@@ -0,0 +1,9 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.io.core.Closeable
interface UpdatesPoller : Closeable {
fun start(scope: CoroutineScope = CoroutineScope(Dispatchers.Default))
}

View File

@@ -0,0 +1,34 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.exceptions
import com.github.insanusmokrassar.TelegramBotAPI.types.Response
import kotlinx.io.errors.IOException
fun newRequestException(
response: Response,
plainAnswer: String,
message: String? = null,
cause: Throwable? = null
) = when (response.description) {
"Bad Request: reply message not found" -> ReplyMessageNotFoundException(response, plainAnswer, message, cause)
"Unauthorized" -> UnauthorizedException(response, plainAnswer, message, cause)
else -> CommonRequestException(response, plainAnswer, message, cause)
}
sealed class RequestException constructor(
val response: Response,
val plainAnswer: String,
message: String? = null,
cause: Throwable? = null
) : IOException(
message ?: "Something went wrong",
cause
)
class CommonRequestException(response: Response, plainAnswer: String, message: String?, cause: Throwable?) :
RequestException(response, plainAnswer, message, cause)
class UnauthorizedException(response: Response, plainAnswer: String, message: String?, cause: Throwable?) :
RequestException(response, plainAnswer, message, cause)
class ReplyMessageNotFoundException(response: Response, plainAnswer: String, message: String?, cause: Throwable?) :
RequestException(response, plainAnswer, message, cause)

View File

@@ -0,0 +1,11 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.settings
import kotlinx.serialization.Serializable
@Serializable
data class ProxySettings(
val host: String = "localhost",
val port: Int = 1080,
val username: String? = null,
val password: String? = null
)

View File

@@ -0,0 +1,67 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.settings.limiters
import com.soywiz.klock.DateTime
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
private fun now(): Long = DateTime.nowUnixLong()
class CommonLimiter(
private val lockCount: Int = 10,
private val regenTime: Long = 20 * 1000L // 20 seconds for full regen of opportunity to send message
) : RequestLimiter {
private var doLimit: Boolean = false
private val counterChannel = Channel<Unit>(Channel.UNLIMITED)
private val scope = CoroutineScope(Dispatchers.Default)
private val counterJob = scope.launch {
var wasLastSecond = 0
var lastCountTime = now()
var limitManagementJob: Job? = null
var removeLimitTime: Long = lastCountTime
for (counter in counterChannel) {
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 {
counterChannel.send(Unit)
return if (!doLimit) {
block()
} else {
quoterChannel.receive()
block()
}
}
}

View File

@@ -0,0 +1,5 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.settings.limiters
object EmptyLimiter : RequestLimiter {
override suspend fun <T> limit(block: suspend () -> T): T = block()
}

View File

@@ -0,0 +1,70 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.settings.limiters
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlin.coroutines.*
import kotlin.math.pow
private sealed class RequestEvent
private class AddRequest(
val continuation: Continuation<Long>
) : RequestEvent()
private object CompleteRequest : RequestEvent()
@Serializable
data class PowLimiter(
private val minAwaitTime: Long = 0L,
private val maxAwaitTime: Long = 10000L,
private val powValue: Double = 4.0,
private val powK: Double = 0.0016
) : RequestLimiter {
@Transient
private val scope = CoroutineScope(Dispatchers.Default)
@Transient
private val eventsChannel = Channel<RequestEvent>(Channel.UNLIMITED)
@Transient
private val awaitTimeRange = minAwaitTime .. maxAwaitTime
init {
scope.launch {
var requestsInWork: Double = 0.0
for (event in eventsChannel) {
when (event) {
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--
}
}
}
}
override suspend fun <T> limit(
block: suspend () -> T
): T {
val delayMillis = suspendCoroutine<Long> {
scope.launch { eventsChannel.send(AddRequest(it)) }
}
delay(delayMillis)
return try {
block()
} finally {
eventsChannel.send(CompleteRequest)
}
}
}

View File

@@ -0,0 +1,8 @@
package com.github.insanusmokrassar.TelegramBotAPI.bot.settings.limiters
interface RequestLimiter {
/**
* Use limit for working of block (like delay between or after, for example)
*/
suspend fun <T> limit(block: suspend () -> T): T
}