chat ui
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
package ru.shadowsparky.http.data
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.http.domain.HTTP_TIMEOUT_SECS
|
||||
import ru.shadowsparky.http.domain.HttpClientFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Factory
|
||||
class AndroidHttpClientFactory(private val settings: HttpClientSettings) : HttpClientFactory {
|
||||
|
||||
override fun create(
|
||||
bearerAuthProvider: BearerAuthProvider?,
|
||||
setupWebSocket: Boolean,
|
||||
setupValidator: Boolean
|
||||
): HttpClient {
|
||||
return HttpClient(OkHttp) {
|
||||
settings.setup(this, bearerAuthProvider, setupWebSocket, setupValidator)
|
||||
engine {
|
||||
config {
|
||||
connectTimeout(HTTP_TIMEOUT_SECS, TimeUnit.SECONDS)
|
||||
readTimeout(HTTP_TIMEOUT_SECS, TimeUnit.SECONDS)
|
||||
writeTimeout(HTTP_TIMEOUT_SECS, TimeUnit.SECONDS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package ru.shadowsparky.http.data
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
actual class HttpClientFactory actual constructor(private val settings: HttpClientSettings) {
|
||||
|
||||
actual fun create(): HttpClient {
|
||||
return HttpClient(OkHttp) {
|
||||
settings.setup(this)
|
||||
engine {
|
||||
config {
|
||||
retryOnConnectionFailure(true)
|
||||
connectTimeout(15, TimeUnit.SECONDS)
|
||||
readTimeout(15, TimeUnit.SECONDS)
|
||||
writeTimeout(15, TimeUnit.SECONDS)
|
||||
}
|
||||
preconfigured = OkHttpClient.Builder()
|
||||
.pingInterval(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,8 @@ import org.koin.core.annotation.ComponentScan
|
||||
import org.koin.core.annotation.Configuration
|
||||
import org.koin.core.annotation.Module
|
||||
import org.koin.core.annotation.Single
|
||||
import ru.shadowsparky.http.data.HttpClientFactory
|
||||
import ru.shadowsparky.http.data.HttpClientSettings
|
||||
import ru.shadowsparky.http.data.RefreshTokenRunner
|
||||
import ru.shadowsparky.http.domain.HttpClientFactory
|
||||
import ru.shadowsparky.http.domain.HttpException
|
||||
import ru.shadowsparky.http.domain.LogoutAction
|
||||
import ru.shadowsparky.http.domain.RefreshTokenAction
|
||||
@@ -25,8 +24,15 @@ import ru.shadowsparky.koin
|
||||
class HttpModule {
|
||||
|
||||
@Single
|
||||
fun createHttpClient(settings: HttpClientSettings): HttpClient {
|
||||
return HttpClientFactory(settings).create()
|
||||
fun createHttpClient(
|
||||
httpClientFactory: HttpClientFactory,
|
||||
bearerAuthProvider: BearerAuthProvider
|
||||
): HttpClient {
|
||||
return httpClientFactory.create(
|
||||
bearerAuthProvider,
|
||||
setupWebSocket = true,
|
||||
setupValidator = true
|
||||
)
|
||||
}
|
||||
|
||||
@Single
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package ru.shadowsparky.http.data
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
|
||||
expect class HttpClientFactory(settings: HttpClientSettings) {
|
||||
fun create(): HttpClient
|
||||
}
|
||||
+33
-29
@@ -24,10 +24,13 @@ import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.domain.AppConfig
|
||||
import ru.shadowsparky.domain.Log
|
||||
import ru.shadowsparky.http.domain.ClientKtorExtension
|
||||
import ru.shadowsparky.http.domain.HTTP_PING_TIMEOUT_SECS
|
||||
import ru.shadowsparky.http.domain.HTTP_TIMEOUT_SECS
|
||||
import ru.shadowsparky.http.domain.HttpException
|
||||
import ru.shadowsparky.http.domain.UserAgentProvider
|
||||
import ru.shadowsparky.koin
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
private const val MESSAGE_LONG = "message"
|
||||
private const val MESSAGE = "msg"
|
||||
@@ -36,39 +39,42 @@ private const val MESSAGE = "msg"
|
||||
class HttpClientSettings(
|
||||
private val json: Json,
|
||||
private val userAgentProvider: UserAgentProvider?,
|
||||
private val bearerAuthProvider: BearerAuthProvider,
|
||||
private val appenderList: List<ClientKtorExtension>,
|
||||
private val log: Log,
|
||||
private val appConfig: AppConfig?
|
||||
) {
|
||||
|
||||
fun setup(
|
||||
clientConfig: HttpClientConfig<*>
|
||||
clientConfig: HttpClientConfig<*>,
|
||||
bearerAuthProvider: BearerAuthProvider?,
|
||||
setupWebSocket: Boolean,
|
||||
setupValidator: Boolean
|
||||
) = with(clientConfig) {
|
||||
install(Auth) {
|
||||
providers.add(bearerAuthProvider)
|
||||
if (bearerAuthProvider != null) {
|
||||
install(Auth) {
|
||||
providers.add(bearerAuthProvider)
|
||||
}
|
||||
}
|
||||
expectSuccess = false
|
||||
|
||||
HttpResponseValidator {
|
||||
validateResponse {
|
||||
val code = it.status.value
|
||||
if (code in 400..599) {
|
||||
val body = it.body<JsonElement>()
|
||||
if (body is JsonObject) {
|
||||
val msg = when {
|
||||
body.containsKey(MESSAGE_LONG) -> body[MESSAGE_LONG]?.jsonPrimitive?.content
|
||||
body.containsKey(MESSAGE) -> body[MESSAGE]?.jsonPrimitive?.content
|
||||
else -> body.toString()
|
||||
if (setupValidator) {
|
||||
expectSuccess = false
|
||||
HttpResponseValidator {
|
||||
validateResponse {
|
||||
val code = it.status.value
|
||||
if (code in 400..599) {
|
||||
val body = it.body<JsonElement>()
|
||||
if (body is JsonObject) {
|
||||
val msg = when {
|
||||
body.containsKey(MESSAGE_LONG) -> body[MESSAGE_LONG]?.jsonPrimitive?.content
|
||||
body.containsKey(MESSAGE) -> body[MESSAGE]?.jsonPrimitive?.content
|
||||
else -> body.toString()
|
||||
}
|
||||
throw HttpException(code, msg)
|
||||
} else {
|
||||
throw HttpException(code, body.toString())
|
||||
}
|
||||
throw HttpException(code, msg)
|
||||
} else {
|
||||
throw HttpException(code, body.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
install(Logging) {
|
||||
val isDebug = koin.getOrNull<AppConfig>()?.debug ?: true
|
||||
if (isDebug) {
|
||||
@@ -85,9 +91,10 @@ class HttpClientSettings(
|
||||
json(json)
|
||||
}
|
||||
|
||||
install(WebSockets) {
|
||||
// как часто слать ping кадры
|
||||
pingIntervalMillis = 30_000 // или: pingInterval = 20.seconds (в новых API)
|
||||
if (setupWebSocket) {
|
||||
install(WebSockets) {
|
||||
pingIntervalMillis = HTTP_PING_TIMEOUT_SECS.seconds.toLong(DurationUnit.MILLISECONDS)
|
||||
}
|
||||
}
|
||||
|
||||
defaultRequest {
|
||||
@@ -98,14 +105,11 @@ class HttpClientSettings(
|
||||
}
|
||||
}
|
||||
|
||||
// Таймауты на уровне Ktor
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS
|
||||
socketTimeoutMillis = HttpTimeoutConfig.INFINITE_TIMEOUT_MS
|
||||
connectTimeoutMillis = 20_000
|
||||
connectTimeoutMillis = HTTP_TIMEOUT_SECS.seconds.toLong(DurationUnit.MILLISECONDS)
|
||||
}
|
||||
|
||||
appenderList.forEach { it.append(this) }
|
||||
}
|
||||
|
||||
private val AppConfig.userAgent: String get() = buildString {
|
||||
|
||||
+21
-22
@@ -1,39 +1,38 @@
|
||||
package ru.shadowsparky.http.data
|
||||
|
||||
import io.ktor.client.plugins.auth.providers.BearerTokens
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.completeWith
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.domain.DispatcherProvider
|
||||
|
||||
|
||||
@Factory
|
||||
class RefreshTokenRunner(private val dispatcherProvider: DispatcherProvider) {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatcherProvider.io)
|
||||
class RefreshTokenRunner {
|
||||
private val lock = Mutex()
|
||||
private var inFlight: Deferred<BearerTokens?>? = null
|
||||
private var inFlight: CompletableDeferred<BearerTokens?>? = null
|
||||
|
||||
suspend fun runOrJoin(block: suspend () -> BearerTokens?): BearerTokens? {
|
||||
val deferred = lock.withLock {
|
||||
inFlight?.takeIf { it.isActive } ?: startNew(block).also { inFlight = it }
|
||||
val myDeferred = lock.withLock {
|
||||
val current = inFlight
|
||||
if (current != null) {
|
||||
return current.await()
|
||||
}
|
||||
CompletableDeferred<BearerTokens?>().also { inFlight = it }
|
||||
}
|
||||
return deferred.await()
|
||||
}
|
||||
|
||||
private fun startNew(block: suspend () -> BearerTokens?): Deferred<BearerTokens?> {
|
||||
val d = scope.async(dispatcherProvider.io) { block() }
|
||||
d.invokeOnCompletion {
|
||||
scope.launch {
|
||||
lock.withLock {
|
||||
if (inFlight === d) inFlight = null
|
||||
try {
|
||||
val tokens = block()
|
||||
myDeferred.complete(tokens)
|
||||
return tokens
|
||||
} catch (e: Throwable) {
|
||||
myDeferred.completeWith(Result.failure(e))
|
||||
throw e
|
||||
} finally {
|
||||
lock.withLock {
|
||||
if (inFlight === myDeferred) {
|
||||
inFlight = null
|
||||
}
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
package ru.shadowsparky.http.domain
|
||||
|
||||
import io.ktor.client.HttpClientConfig
|
||||
|
||||
interface ClientKtorExtension {
|
||||
fun append(clientConfig: HttpClientConfig<*>)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package ru.shadowsparky.http.domain
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
|
||||
|
||||
internal const val HTTP_TIMEOUT_SECS = 30L
|
||||
internal const val HTTP_PING_TIMEOUT_SECS = 30L
|
||||
|
||||
interface HttpClientFactory {
|
||||
fun create(
|
||||
bearerAuthProvider: BearerAuthProvider? = null,
|
||||
setupWebSocket: Boolean = true,
|
||||
setupValidator: Boolean = true
|
||||
): HttpClient
|
||||
}
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package ru.shadowsparky.http.data
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
actual class HttpClientFactory actual constructor(private val settings: HttpClientSettings) {
|
||||
actual fun create(): HttpClient {
|
||||
return HttpClient(OkHttp) {
|
||||
settings.setup(this)
|
||||
engine {
|
||||
config {
|
||||
retryOnConnectionFailure(true)
|
||||
connectTimeout(15, TimeUnit.SECONDS)
|
||||
readTimeout(15, TimeUnit.SECONDS)
|
||||
writeTimeout(15, TimeUnit.SECONDS)
|
||||
}
|
||||
preconfigured = OkHttpClient.Builder().pingInterval(30, TimeUnit.SECONDS).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package ru.shadowsparky.http.data
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.http.domain.HTTP_TIMEOUT_SECS
|
||||
import ru.shadowsparky.http.domain.HttpClientFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.security.KeyStore
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.SSLSocketFactory
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
@Factory
|
||||
class JvmHttpClientFactory(private val settings: HttpClientSettings) : HttpClientFactory {
|
||||
|
||||
override fun create(
|
||||
bearerAuthProvider: BearerAuthProvider?,
|
||||
setupWebSocket: Boolean,
|
||||
setupValidator: Boolean
|
||||
): HttpClient {
|
||||
return HttpClient(OkHttp) {
|
||||
settings.setup(this, bearerAuthProvider, setupWebSocket, setupValidator)
|
||||
engine {
|
||||
config {
|
||||
connectTimeout(HTTP_TIMEOUT_SECS, TimeUnit.SECONDS)
|
||||
readTimeout(HTTP_TIMEOUT_SECS, TimeUnit.SECONDS)
|
||||
writeTimeout(HTTP_TIMEOUT_SECS, TimeUnit.SECONDS)
|
||||
val (sslSocketFactory, trustManager) = createSslConfig()
|
||||
sslSocketFactory(sslSocketFactory, trustManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createSslConfig(): Pair<SSLSocketFactory, X509TrustManager> {
|
||||
val defaultTmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
|
||||
val defaultTmf = TrustManagerFactory.getInstance(defaultTmfAlgorithm).apply {
|
||||
init(null as KeyStore?)
|
||||
}
|
||||
val defaultX509TrustManager = defaultTmf.trustManagers
|
||||
.first { it is X509TrustManager } as X509TrustManager
|
||||
val keyStoreType = KeyStore.getDefaultType()
|
||||
val combinedKeyStore = KeyStore.getInstance(keyStoreType).apply {
|
||||
load(null, null)
|
||||
}
|
||||
defaultX509TrustManager.acceptedIssuers.forEachIndexed { index, cert ->
|
||||
combinedKeyStore.setCertificateEntry("system_ca_$index", cert)
|
||||
}
|
||||
val cf = CertificateFactory.getInstance("X.509")
|
||||
val certInputStream = ByteArrayInputStream(CUSTOM_PEM_CERTIFICATE.trimIndent().toByteArray(Charsets.UTF_8))
|
||||
val customCertificate = cf.generateCertificate(certInputStream) as X509Certificate
|
||||
combinedKeyStore.setCertificateEntry("russian_trusted_root_ca", customCertificate)
|
||||
val combinedTmf = TrustManagerFactory.getInstance(defaultTmfAlgorithm).apply {
|
||||
init(combinedKeyStore)
|
||||
}
|
||||
val combinedTrustManagers = combinedTmf.trustManagers
|
||||
val finalX509TrustManager = combinedTrustManagers.first { it is X509TrustManager } as X509TrustManager
|
||||
val sslContext = SSLContext.getInstance("TLS").apply {
|
||||
init(null, combinedTrustManagers, null)
|
||||
}
|
||||
return Pair(sslContext.socketFactory, finalX509TrustManager)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CUSTOM_PEM_CERTIFICATE = """
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
|
||||
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
|
||||
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
|
||||
Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
|
||||
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
|
||||
YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
|
||||
dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
|
||||
qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
|
||||
XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
|
||||
zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
|
||||
YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
|
||||
Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
|
||||
U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
|
||||
4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
|
||||
G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
|
||||
BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
|
||||
ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
|
||||
OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
|
||||
BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
|
||||
BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
|
||||
AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
|
||||
tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
|
||||
W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
|
||||
/3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
|
||||
AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
|
||||
C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
|
||||
4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
|
||||
WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
|
||||
D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
|
||||
EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
|
||||
391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
|
||||
-----END CERTIFICATE-----
|
||||
"""
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package ru.shadowsparky.http.data
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.js.Js
|
||||
|
||||
actual class HttpClientFactory actual constructor(private val settings: HttpClientSettings) {
|
||||
actual fun create(): HttpClient {
|
||||
return HttpClient(Js) {
|
||||
settings.setup(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package ru.shadowsparky.http.data
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.js.Js
|
||||
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
|
||||
import org.koin.core.annotation.Factory
|
||||
import ru.shadowsparky.http.domain.HttpClientFactory
|
||||
|
||||
@Factory
|
||||
class WasmHttpClientFactory(private val settings: HttpClientSettings) : HttpClientFactory {
|
||||
override fun create(
|
||||
bearerAuthProvider: BearerAuthProvider?,
|
||||
setupWebSocket: Boolean,
|
||||
setupValidator: Boolean
|
||||
): HttpClient {
|
||||
return HttpClient(Js) {
|
||||
settings.setup(this, bearerAuthProvider, setupWebSocket, setupValidator)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user