Skip to content
This repository has been archived by the owner on Apr 9, 2024. It is now read-only.

Updates the RequestHandler to use the config obj #78

Merged
merged 6 commits into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion clashJ/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ dependencies {
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.cio)
implementation(libs.ktor.client.logging)
implementation(libs.ktor.client.apache5)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.gson)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package io.github.maicolantali.http

import ch.qos.logback.classic.Level
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import io.github.maicolantali.exception.BadGatewayException
Expand All @@ -9,26 +8,20 @@ import io.github.maicolantali.exception.HttpException
import io.github.maicolantali.exception.InvalidCredentialException
import io.github.maicolantali.exception.MaintenanceException
import io.github.maicolantali.exception.NotFoundException
import io.github.maicolantali.http.option.EngineOptions
import io.github.maicolantali.http.option.KeyOptions
import io.github.maicolantali.http.option.RequestOptions
import io.github.maicolantali.http.response.CreateKeyResponse
import io.github.maicolantali.http.response.KeyListResponse
import io.github.maicolantali.http.response.base.Key
import io.github.maicolantali.http.throttler.BaseThrottler
import io.github.maicolantali.types.internal.configuration.ClientConfiguration
import io.github.maicolantali.util.API_BASE_URL
import io.github.maicolantali.util.Credential
import io.github.maicolantali.util.DEV_SITE_BASE_URL
import io.github.maicolantali.util.SafeSet
import io.github.maicolantali.util.getConfiguredHttpClient
import io.github.maicolantali.util.setLevel
import io.github.oshai.kotlinlogging.KotlinLogging
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.plugins.HttpRequestTimeoutException
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.request.post
import io.ktor.client.request.request
import io.ktor.client.request.setBody
Expand All @@ -37,7 +30,6 @@ import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.serialization.gson.gson
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
Expand All @@ -51,43 +43,31 @@ import java.util.concurrent.atomic.AtomicBoolean
* Handles HTTP requests to and from [DEV_SITE_BASE_URL] and [API_BASE_URL].
*
* @param credential The `Credential` object containing the email and password for authentication.
* @param keyOptions The `KeyOptions` specifying the key name, optional description, and key count.
* @param engineOptions The `EngineOptions` specifying the connection timeout and request timeout.
* @param engine The `HttpClientEngine` to be used for HTTP requests.
* @param throttler The `BaseThrottler` instance responsible for controlling the rate of API requests.
*/
class RequestHandler(
private val credential: Credential,
private val keyOptions: KeyOptions,
private val engineOptions: EngineOptions,
private val engine: HttpClientEngine,
private val throttler: BaseThrottler,
internal val config: ClientConfiguration,
) {
private companion object {
private const val MAX_KEY_COUNT = 10
}

private val keysSet = SafeSet<String>()
private val isLoginInProgress = AtomicBoolean(false)
private val logger = KotlinLogging.logger {}
private val isLoginInProgress = AtomicBoolean(false)

private val keysSet = SafeSet<String>()
private val httpClient by lazy { getConfiguredHttpClient() }

// Config
private val keyName by lazy { config.key.keyName }
private val keyDescription by lazy { config.key.keyDescription }
private val keyCount by lazy { config.key.keyCount }
private val throttler by lazy { config.throttler }

init {
logger.setLevel(Level.TRACE)
logger.setLevel(config.logging.clientLogLevel)
}

private val httpClient =
HttpClient(engine) {
install(HttpTimeout) {
requestTimeoutMillis = engineOptions.requestTimeout
connectTimeoutMillis = engineOptions.requestTimeout
}
install(ContentNegotiation) { gson() }
defaultRequest {
// Set for all requests the content type to: application/json
contentType(ContentType.Application.Json)
}
}

/**
* Performs the login to the developer site using the provided email and password.
*
Expand All @@ -112,6 +92,7 @@ class RequestHandler(
loginResponse =
httpClient.post("$DEV_SITE_BASE_URL/login") {
setBody(credential)
contentType(ContentType.Application.Json)
}
break // Success, exit the loop
} catch (e: HttpRequestTimeoutException) {
Expand Down Expand Up @@ -203,6 +184,7 @@ class RequestHandler(
httpClient.request(url) {
method = requestOptions.method
setBody(requestOptions.body)
contentType(ContentType.Application.Json)
headers.append(HttpHeaders.Authorization, "Bearer ${keysSet.next()}")
}

Expand All @@ -221,7 +203,7 @@ class RequestHandler(
login()
isLoginInProgress.set(false)
} else {
delay(engineOptions.requestTimeout * (tries + 1))
delay(config.httpClient.socketTimeoutMillis * (tries + 1))
}

continue
Expand Down Expand Up @@ -303,7 +285,7 @@ class RequestHandler(
logger.info { "${keys.size} valid keys retrieved from the developer site." }

// Revoke keys that do not have the current IP
keys.filter { it.name == keyOptions.keyName }
keys.filter { it.name == keyName }
.filter { !it.cidrRanges.contains(currentIp) }
.map { key ->
launch {
Expand All @@ -316,9 +298,9 @@ class RequestHandler(
}.joinAll()

// Append keys that have the current IP
keys.filter { it.name == keyOptions.keyName }
keys.filter { it.name == keyName }
.filter { it.cidrRanges.contains(currentIp) }
.takeWhile { keysSet.size() < keyOptions.keyCount }
.takeWhile { keysSet.size() < keyCount }
.map { key ->
launch {
keysSet.add(key.key)
Expand All @@ -327,23 +309,23 @@ class RequestHandler(
}.joinAll()

// Create new keys within limits of 10 keys per account
while (keysSet.size() < keyOptions.keyCount && keys.size < MAX_KEY_COUNT) {
while (keysSet.size() < keyCount && keys.size < MAX_KEY_COUNT) {
val newKey = createKey(currentIp, cookies)
keysSet.add(newKey.key)
keys.add(newKey)
logger.info { "Created a new key '${newKey.key}' and added it to the key set." }
}

if (keysSet.size() < keyOptions.keyCount && keys.size == 10) {
if (keysSet.size() < keyCount && keys.size == 10) {
val warnMsg =
"Required ${keyOptions.keyCount} keys, but maximum ${keysSet.size()} found/made (limit: 10/acc). " +
"Required $keyCount keys, but maximum ${keysSet.size()} found/made (limit: 10/acc). " +
"Please delete keys or decrease `keyCount`."
logger.warn { warnMsg }
}

if (keysSet.size() == 0) {
val errorMessage =
"${keys.size} existing API keys, none match keyName: ${keyOptions.keyName} " +
"${keys.size} existing API keys, none match keyName: $keyName " +
"Specify another keyName or delete unused keys at 'https://developer.clashofclans.com'."
logger.error { errorMessage }
throw ClashJException(errorMessage)
Expand All @@ -367,20 +349,14 @@ class RequestHandler(
ip: String,
cookie: String,
): Key {
val keyJson =
"""{"cidrRanges": ["$ip"],"name": "${keyOptions.keyName}","description": "${
if (!keyOptions.keyDescription.isNullOrBlank()) {
keyOptions.keyDescription
} else {
LocalDateTime.now()
}
}"}"""
val keyJson = """{"cidrRanges": ["$ip"],"name": "$keyName","description": "${keyDescription ?: LocalDateTime.now()}"}"""

logger.info { "Generating a new API key with the following data: $keyJson" }

val response: CreateKeyResponse =
httpClient.post("$DEV_SITE_BASE_URL/apikey/create") {
setBody(keyJson)
contentType(ContentType.Application.Json)
headers.append(HttpHeaders.Cookie, cookie)
}.body()

Expand Down Expand Up @@ -415,6 +391,7 @@ class RequestHandler(
val response =
httpClient.post("${DEV_SITE_BASE_URL}/apikey/revoke") {
setBody("""{"id": "${key.id}"}""")
contentType(ContentType.Application.Json)
headers.append(HttpHeaders.Cookie, cookie)
}

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package io.github.maicolantali.types.internal.configuration

import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.apache5.Apache5
import io.ktor.client.engine.cio.CIO

/**
* Configuration class for HTTP settings.
* Configuration data class for HTTP client settings.
*
* @property engine The HTTP client engine to be used. Default is Apache5.
* @property connectionTimeout The maximum time, in milliseconds, to wait for a connection to be established.
* Default is 15,000 (15 seconds).
* @property requestTimeout The maximum time, in milliseconds, to wait for a request to complete.
* Default is 15,000 (15 seconds).
* @property engine The HTTP client engine to use. Defaults to [CIO.create].
* @property socketTimeoutMillis The socket timeout in milliseconds. Defaults to 5,000 milliseconds.
* @property connectionTimeout The connection timeout in milliseconds. Defaults to 5,000 milliseconds.
* @property requestTimeout The request timeout in milliseconds. Defaults to 10,000 milliseconds.
*/
data class HttpConfiguration(
var engine: HttpClientEngine = Apache5.create(),
var connectionTimeout: Long = 15_000,
var requestTimeout: Long = 15_000,
var engine: HttpClientEngine = CIO.create(),
var socketTimeoutMillis: Long = 5_000,
var connectionTimeout: Long = 5_000,
var requestTimeout: Long = 10_000,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,34 @@ package io.github.maicolantali.util

import io.github.maicolantali.Client
import io.github.maicolantali.http.RequestHandler
import io.github.maicolantali.http.option.EngineOptions
import io.github.maicolantali.http.option.KeyOptions
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.Logging
import io.ktor.serialization.gson.gson
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors

internal fun Client.getConfiguredRequestHandler() =
with(config) {
RequestHandler(
Credential(email, password),
KeyOptions(key.keyName, key.keyDescription, key.keyCount),
EngineOptions(httpClient.connectionTimeout, httpClient.requestTimeout),
httpClient.engine,
throttler,
)
}
internal fun Client.getConfiguredRequestHandler() = RequestHandler(Credential(email, password), config)

/**
* Retrieves a coroutine dispatcher configured based on the client's thread configuration.
*
* @return A coroutine dispatcher configured with the specified number of threads.
*/
internal fun Client.getConfiguredDispatcher() = Executors.newFixedThreadPool(config.nThread).asCoroutineDispatcher()

internal fun RequestHandler.getConfiguredHttpClient() =
HttpClient(config.httpClient.engine) {
install(Logging) {
level = config.logging.httClientLogLevel.toKtorLogLevel()
}

install(HttpTimeout) {
socketTimeoutMillis = config.httpClient.socketTimeoutMillis
connectTimeoutMillis = config.httpClient.connectionTimeout
requestTimeoutMillis = config.httpClient.requestTimeout
}

install(ContentNegotiation) { gson() }
}
Loading