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:
Jorge Martín
2026-08-10 14:22:39 +02:00
committed by Jorge Martin Espinosa
parent 3f19100427
commit 25eca29cd2
5 changed files with 137 additions and 41 deletions
+4
View File
@@ -12,3 +12,7 @@ plugins {
android {
namespace = "io.element.android.libraries.wellknown.api"
}
dependencies {
implementation(projects.libraries.matrix.api)
}
@@ -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
}
}
@@ -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
}
}
@@ -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
@@ -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
}
}
}