shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/APIClient.kt (134 lines of code) (raw):
@file:OptIn(ExperimentalTime::class)
package org.jetbrains.kotlinconf
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.DefaultRequest
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logging
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType
import io.ktor.http.HttpHeaders
import io.ktor.http.contentType
import io.ktor.http.encodedPath
import io.ktor.http.isSuccess
import io.ktor.http.takeFrom
import io.ktor.serialization.kotlinx.json.json
import io.ktor.utils.io.core.Closeable
import kotlinx.coroutines.CancellationException
import org.jetbrains.kotlinconf.utils.Logger
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
import io.ktor.client.plugins.logging.Logger as KtorLogger
/**
* Adapter to handle backend API and manage auth information.
*/
class APIClient(
private val apiUrl: String,
private val appLogger: Logger,
) : Closeable {
companion object {
private const val LOG_TAG = "APIClient"
}
var userId: String? = null
private val client = HttpClient {
install(ContentNegotiation) {
json()
}
install(Logging) {
level = LogLevel.HEADERS
logger = object : KtorLogger {
override fun log(message: String) {
appLogger.log(LOG_TAG) { message }
}
}
}
expectSuccess = true
install(HttpTimeout) {
requestTimeoutMillis = 5000
}
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
exponentialDelay()
}
install(DefaultRequest) {
url.takeFrom(apiUrl)
}
}
/**
* @return status of request.
*/
suspend fun sign(userId :String): Boolean {
return safeApiCall {
client.post {
apiUrl("sign")
setBody(userId)
}.status.isSuccess()
} ?: false
}
/**
* Get [ConferenceData] info
*/
suspend fun downloadConferenceData(): Conference? {
return safeApiCall {
client.get("conference").body()
}
}
/**
* Vote for session.
*/
suspend fun vote(sessionId: SessionId, score: Score?): Boolean {
if (userId == null) return false
return safeApiCall {
client.post {
apiUrl("vote")
json()
setBody(VoteInfo(sessionId, score))
}.status.isSuccess()
} ?: false
}
/**
* Send feedback
*/
suspend fun sendFeedback(sessionId: SessionId, feedback: String): Boolean {
if (userId == null) return false
return safeApiCall {
client.post {
apiUrl("feedback")
json()
setBody(FeedbackInfo(sessionId, feedback))
}.status.isSuccess()
} ?: false
}
/**
* List my votes.
*/
suspend fun myVotes(): List<VoteInfo> {
if (userId == null) return emptyList()
return safeApiCall {
client.get { apiUrl("vote") }.body<Votes>().votes
} ?: emptyList()
}
suspend fun getServerTime(): Instant? {
return safeApiCall {
client.get { apiUrl("time") }.bodyAsText()
.let { response -> Instant.fromEpochMilliseconds(response.toLong()) }
}
}
/**
* Runs the [call], returning its result or `null` if exceptions occurred.
*/
private suspend fun <T> safeApiCall(
call: suspend () -> T,
): T? {
return try {
call()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
appLogger.log(LOG_TAG) { "API call failed: ${e.message}" }
null
}
}
private fun HttpRequestBuilder.json() {
contentType(ContentType.Application.Json)
}
private fun HttpRequestBuilder.apiUrl(path: String) {
if (userId != null) {
header(HttpHeaders.Authorization, "Bearer $userId")
}
header(HttpHeaders.CacheControl, "no-cache")
url {
encodedPath = path
}
}
override fun close() {
client.close()
}
}