mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Rework WellknownRetriever so it uses the GetUrlResolver
Also, allow instantiating it using a factory: with this we can always decide where to take the config from and how to do it Do the same for `ElementWellknownStore` so we can store and restore the values for different sources (endpoints) independently
This commit is contained in:
committed by
Jorge Martin Espinosa
parent
3f19100427
commit
25eca29cd2
@@ -12,3 +12,7 @@ plugins {
|
||||
android {
|
||||
namespace = "io.element.android.libraries.wellknown.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
||||
+4
@@ -11,4 +11,8 @@ interface ElementWellknownStore {
|
||||
suspend fun get(domain: String): WellknownRetrieverResult<ElementWellKnown>
|
||||
suspend fun update(domain: String, wellknown: String): Result<Unit>
|
||||
suspend fun delete(domain: String): Result<Unit>
|
||||
|
||||
fun interface Factory {
|
||||
fun create(prefix: String?): ElementWellknownStore
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -8,6 +8,15 @@
|
||||
|
||||
package io.element.android.libraries.wellknown.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.GetUrlResolver
|
||||
|
||||
interface WellknownRetriever {
|
||||
suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult<ElementWellKnown>
|
||||
suspend fun getElementWellKnown(
|
||||
host: String,
|
||||
source: EnterpriseRemoteConfigSource
|
||||
): WellknownRetrieverResult<ElementWellKnown>
|
||||
|
||||
interface Factory {
|
||||
fun create(getUrlResolver: GetUrlResolver): WellknownRetriever
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -8,6 +8,9 @@
|
||||
package io.element.android.libraries.wellknown.impl
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.cachestore.api.CacheData
|
||||
import io.element.android.libraries.cachestore.api.CacheStore
|
||||
@@ -18,12 +21,19 @@ import io.element.android.libraries.wellknown.api.ElementWellknownStore
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@AssistedInject
|
||||
class DefaultWellKnownStore(
|
||||
@Assisted
|
||||
private val prefix: String?,
|
||||
private val cacheStore: CacheStore,
|
||||
private val elementWellKnownParser: ElementWellKnownParser,
|
||||
private val systemClock: SystemClock,
|
||||
) : ElementWellknownStore {
|
||||
@AssistedFactory
|
||||
@ContributesBinding(AppScope::class)
|
||||
fun interface Factory : ElementWellknownStore.Factory {
|
||||
override fun create(prefix: String?): DefaultWellKnownStore
|
||||
}
|
||||
override suspend fun get(domain: String): WellknownRetrieverResult<ElementWellKnown> {
|
||||
return runCatchingExceptions {
|
||||
val cachedData = cacheStore.getData(key(domain))
|
||||
@@ -54,7 +64,11 @@ class DefaultWellKnownStore(
|
||||
}
|
||||
}
|
||||
|
||||
private fun key(domain: String): String = "https://$domain/.well-known/element/element.json"
|
||||
private fun key(domain: String): String = if (prefix != null) {
|
||||
"$prefix-$domain"
|
||||
} else {
|
||||
"https://$domain/.well-known/element/element.json"
|
||||
}
|
||||
|
||||
companion object {
|
||||
// 1 day
|
||||
|
||||
+103
-38
@@ -9,65 +9,130 @@
|
||||
package io.element.android.libraries.wellknown.impl
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Assisted
|
||||
import dev.zacsweers.metro.AssistedFactory
|
||||
import dev.zacsweers.metro.AssistedInject
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||
import io.element.android.libraries.core.extensions.mapCatchingExceptions
|
||||
import io.element.android.libraries.core.uri.ensureProtocol
|
||||
import io.element.android.libraries.network.RetrofitFactory
|
||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||
import io.element.android.libraries.matrix.api.GetUrlResolver
|
||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
|
||||
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
||||
import io.element.android.libraries.wellknown.api.ElementWellknownStore
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||
import retrofit2.HttpException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
@AssistedInject
|
||||
class DefaultWellknownRetriever(
|
||||
private val retrofitFactory: RetrofitFactory,
|
||||
private val elementWellknownStore: ElementWellknownStore,
|
||||
private val jsonProvider: JsonProvider,
|
||||
private val elementWellknownStoreFactory: ElementWellknownStore.Factory,
|
||||
private val enterpriseService: EnterpriseService,
|
||||
private val elementWellKnownParser: ElementWellKnownParser,
|
||||
@Assisted private val getUrlResolver: GetUrlResolver,
|
||||
@AppCoroutineScope private val coroutineScope: CoroutineScope,
|
||||
) : WellknownRetriever {
|
||||
override suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult<ElementWellKnown> {
|
||||
@ContributesBinding(AppScope::class)
|
||||
@AssistedFactory
|
||||
fun interface Factory : WellknownRetriever.Factory {
|
||||
override fun create(getUrlResolver: GetUrlResolver): DefaultWellknownRetriever
|
||||
}
|
||||
|
||||
override suspend fun getElementWellKnown(
|
||||
host: String,
|
||||
source: EnterpriseRemoteConfigSource
|
||||
): WellknownRetrieverResult<ElementWellKnown> {
|
||||
val overriddenElementWellKnown = enterpriseService.overriddenElementWellKnown()
|
||||
if (overriddenElementWellKnown != null) {
|
||||
return WellknownRetrieverResult.Success(overriddenElementWellKnown)
|
||||
}
|
||||
|
||||
val domain = URL(baseUrl).host
|
||||
return buildWellknownApi(baseUrl)
|
||||
.map { wellknownApi ->
|
||||
try {
|
||||
val json = jsonProvider()
|
||||
val result = wellknownApi.getElementWellKnown()
|
||||
elementWellknownStore.update(domain, json.encodeToString(result))
|
||||
WellknownRetrieverResult.Success(result.map())
|
||||
} catch (e: Exception) {
|
||||
// Is it a 404?
|
||||
Timber.e(e, "Failed to retrieve Element well-known data for $baseUrl")
|
||||
if ((e as? HttpException)?.code() == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||
WellknownRetrieverResult.NotFound
|
||||
} else {
|
||||
WellknownRetrieverResult.Error(e)
|
||||
}
|
||||
}
|
||||
val checkedHost = URL(host.ensureProtocol()).host ?: host
|
||||
|
||||
// We instantiate different stores for different sources, so that we can cache them separately.
|
||||
// The ESS_CONFIG source is cached with a prefix to differentiate it from the WELLKNOWN_ENDPOINT.
|
||||
val prefix = when (source) {
|
||||
EnterpriseRemoteConfigSource.ESS_CONFIG -> "ess_config"
|
||||
EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT -> null
|
||||
}
|
||||
val store = elementWellknownStoreFactory.create(prefix)
|
||||
return when (val cacheData = store.get(checkedHost)) {
|
||||
is WellknownRetrieverResult.Success -> {
|
||||
Timber.d("Using cached well-known for domain $checkedHost")
|
||||
fetchElementWellKnown(checkedHost, source, store)
|
||||
cacheData
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { it },
|
||||
onFailure = { WellknownRetrieverResult.Error(it as Exception) }
|
||||
)
|
||||
is WellknownRetrieverResult.Outdated -> {
|
||||
// Return the outdated data but refresh in the background
|
||||
// If the cache is missing or outdated, trigger a refresh in background but still return the cached value
|
||||
Timber.d("Outdated cached well-known for domain $checkedHost, returning existing value and fetching new one from network")
|
||||
coroutineScope.launch { fetchElementWellKnown(checkedHost, source, store) }
|
||||
cacheData
|
||||
}
|
||||
is WellknownRetrieverResult.NotFound -> {
|
||||
// Try to fetch from the server
|
||||
Timber.d("No cached well-known for domain $checkedHost, fetching from network")
|
||||
fetchElementWellKnown(checkedHost, source, store)
|
||||
}
|
||||
is WellknownRetrieverResult.Error -> {
|
||||
// Return the error
|
||||
Timber.e(cacheData.exception, "Error retrieving well-known for domain $checkedHost")
|
||||
cacheData.exception.toWellknownRetrieverResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildWellknownApi(accountProviderUrl: String): Result<WellknownAPI> {
|
||||
return runCatchingExceptions {
|
||||
retrofitFactory.create(accountProviderUrl.ensureProtocol())
|
||||
.create(WellknownAPI::class.java)
|
||||
}.onFailure { e ->
|
||||
// If the base URL is not valid, we cannot retrieve the well-known data
|
||||
Timber.e(e, "Failed to create Retrofit instance for $accountProviderUrl")
|
||||
private suspend fun fetchElementWellKnown(
|
||||
host: String,
|
||||
source: EnterpriseRemoteConfigSource,
|
||||
store: ElementWellknownStore
|
||||
): WellknownRetrieverResult<ElementWellKnown> {
|
||||
return getUrlResolver
|
||||
.getUrl(url(host, source))
|
||||
.mapCatchingExceptions {
|
||||
val data = String(it)
|
||||
val parsed = elementWellKnownParser.parse(data).getOrThrow()
|
||||
// Also store in cache, if valid
|
||||
store.update(host, data)
|
||||
.onFailure { exception ->
|
||||
Timber.e(exception, "Failed to parse cached Element .well-known data for $host, deleting cache")
|
||||
store.delete(host)
|
||||
}
|
||||
parsed
|
||||
}
|
||||
.toWellknownRetrieverResult(host)
|
||||
}
|
||||
|
||||
private fun <T> Result<T>.toWellknownRetrieverResult(host: String): WellknownRetrieverResult<T> = fold(
|
||||
onSuccess = {
|
||||
WellknownRetrieverResult.Success(it)
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Failed to retrieve Element .well-known from $host")
|
||||
// This check on message value is not ideal but this is what we got from the SDK.
|
||||
it.toWellknownRetrieverResult()
|
||||
}
|
||||
)
|
||||
|
||||
private fun <T> Throwable.toWellknownRetrieverResult(): WellknownRetrieverResult<T> {
|
||||
return if ((this as? ClientException.Generic)?.message?.contains("404") == true) {
|
||||
WellknownRetrieverResult.NotFound
|
||||
} else {
|
||||
WellknownRetrieverResult.Error(this as Exception)
|
||||
}
|
||||
}
|
||||
|
||||
private fun url(host: String, source: EnterpriseRemoteConfigSource): String {
|
||||
val elementWellKnownUrl = "https://$host/.well-known/element/element.json"
|
||||
return when (source) {
|
||||
EnterpriseRemoteConfigSource.ESS_CONFIG -> enterpriseService.essConfigEndpointUrl(host) ?: elementWellKnownUrl
|
||||
EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT -> elementWellKnownUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user