commonMain.net.folivo.trixnity.clientserverapi.client.MatrixClientServerApiHttpClient.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of trixnity-clientserverapi-client Show documentation
Show all versions of trixnity-clientserverapi-client Show documentation
Multiplatform Kotlin SDK for matrix-protocol
package net.folivo.trixnity.clientserverapi.client
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.resources.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.serializer
import net.folivo.trixnity.api.client.MatrixApiClient
import net.folivo.trixnity.api.client.defaultTrixnityHttpClientFactory
import net.folivo.trixnity.clientserverapi.model.uia.*
import net.folivo.trixnity.core.ErrorResponse
import net.folivo.trixnity.core.ErrorResponseSerializer
import net.folivo.trixnity.core.HttpMethod
import net.folivo.trixnity.core.MatrixServerException
import net.folivo.trixnity.core.serialization.createMatrixEventJson
import net.folivo.trixnity.core.serialization.events.DefaultEventContentSerializerMappings
import net.folivo.trixnity.core.serialization.events.EventContentSerializerMappings
class MatrixClientServerApiHttpClient(
private val baseUrl: Url? = null,
eventContentSerializerMappings: EventContentSerializerMappings = DefaultEventContentSerializerMappings,
json: Json = createMatrixEventJson(eventContentSerializerMappings),
accessToken: MutableStateFlow,
private val onLogout: suspend (isSoft: Boolean) -> Unit = {},
httpClientFactory: (config: HttpClientConfig<*>.() -> Unit) -> HttpClient = defaultTrixnityHttpClientFactory(),
) : MatrixApiClient(
eventContentSerializerMappings,
json,
{
httpClientFactory {
it()
install(DefaultRequest) {
accessToken.value?.let { bearerAuth(it) }
if (baseUrl != null) url.takeFrom(baseUrl)
}
install(ConvertMediaPlugin)
install(HttpRequestRetry) {
retryIf { _, httpResponse ->
httpResponse.status == HttpStatusCode.TooManyRequests
}
retryOnExceptionIf { _, throwable ->
throwable is MatrixServerException && throwable.statusCode == HttpStatusCode.TooManyRequests
}
exponentialDelay(maxDelayMs = 30_000, respectRetryAfterHeader = true)
}
}
}
) {
override suspend fun onErrorResponse(response: HttpResponse, errorResponse: ErrorResponse) {
if (response.status == HttpStatusCode.Unauthorized && errorResponse is ErrorResponse.UnknownToken) {
onLogout(errorResponse.softLogout)
}
}
@OptIn(ExperimentalSerializationApi::class)
suspend inline fun , reified REQUEST, reified RESPONSE> uiaRequest(
endpoint: ENDPOINT,
requestBody: REQUEST,
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
): Result> {
val requestSerializer = endpoint.plainRequestSerializerBuilder(contentMappings, json) ?: serializer()
val responseSerializer = endpoint.plainResponseSerializerBuilder(contentMappings, json) ?: serializer()
return uiaRequest(requestBody, requestSerializer, responseSerializer) { jsonBody ->
baseClient.request(endpoint) {
val endpointHttpMethod =
serializer().descriptor.annotations.filterIsInstance().firstOrNull()
?: throw IllegalArgumentException("matrix endpoint needs @Method annotation")
method = io.ktor.http.HttpMethod(endpointHttpMethod.type.name)
endpoint.requestContentType?.let { contentType(it) }
endpoint.responseContentType?.let { accept(it) }
setBody(jsonBody)
requestBuilder()
}.body()
}
}
suspend inline fun , reified RESPONSE> uiaRequest(
endpoint: ENDPOINT,
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
): Result> = uiaRequest(endpoint, Unit, requestBuilder)
@PublishedApi
internal suspend fun uiaRequest(
requestBody: REQUEST,
requestSerializer: KSerializer,
responseSerializer: KSerializer,
jsonRequest: suspend (body: JsonObject) -> JsonObject
): Result> = kotlin.runCatching {
val jsonBody = json.encodeToJsonElement(requestSerializer, requestBody)
require(jsonBody is JsonObject)
try {
val plainResponse = jsonRequest(jsonBody)
UIA.Success(json.decodeFromJsonElement(responseSerializer, plainResponse))
} catch (responseException: ResponseException) {
val response = responseException.response
val responseText = response.bodyAsText()
if (response.status == HttpStatusCode.Unauthorized) {
val responseObject = json.decodeFromString(responseText)
val state = json.decodeFromJsonElement(responseObject)
val errorCode = responseObject["errcode"]
val getFallbackUrl: (AuthenticationType) -> Url = { authenticationType ->
URLBuilder().apply {
val localBaseUlr = baseUrl
if (localBaseUlr != null) takeFrom(localBaseUlr)
encodedPath += "_matrix/client/v3/auth/${authenticationType.name}/fallback/web"
state.session?.let { parameters.append("session", it) }
}.build()
}
val authenticate: suspend (AuthenticationRequest) -> Result> = { authenticationRequest ->
uiaRequest(
RequestWithUIA(
jsonBody,
AuthenticationRequestWithSession(authenticationRequest, state.session)
),
serializer(),
responseSerializer,
jsonRequest
)
}
if (errorCode != null) {
val error = json.decodeFromJsonElement(responseObject)
if (error is ErrorResponse.UnknownToken) {
onLogout(error.softLogout)
}
UIA.Error(state, error, getFallbackUrl, authenticate)
} else {
UIA.Step(state, getFallbackUrl, authenticate)
}
} else {
val errorResponse =
try {
json.decodeFromString(
ErrorResponseSerializer,
responseText
)
} catch (error: Throwable) {
ErrorResponse.CustomErrorResponse(
"UNKNOWN",
responseText
)
}
throw MatrixServerException(response.status, errorResponse)
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy