From 25eca29cd2bc83656697ef6eb89f2a5bf2218f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 10 Aug 2026 14:22:39 +0200 Subject: [PATCH] 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 --- libraries/wellknown/api/build.gradle.kts | 4 + .../wellknown/api/ElementWellknownStore.kt | 4 + .../wellknown/api/WellknownRetriever.kt | 11 +- .../wellknown/impl/DefaultWellKnownStore.kt | 18 ++- .../impl/DefaultWellknownRetriever.kt | 141 +++++++++++++----- 5 files changed, 137 insertions(+), 41 deletions(-) diff --git a/libraries/wellknown/api/build.gradle.kts b/libraries/wellknown/api/build.gradle.kts index 768214d874..54da0d1445 100644 --- a/libraries/wellknown/api/build.gradle.kts +++ b/libraries/wellknown/api/build.gradle.kts @@ -12,3 +12,7 @@ plugins { android { namespace = "io.element.android.libraries.wellknown.api" } + +dependencies { + implementation(projects.libraries.matrix.api) +} diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt index 80b2ab4c5e..abb1cf53c7 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellknownStore.kt @@ -11,4 +11,8 @@ interface ElementWellknownStore { suspend fun get(domain: String): WellknownRetrieverResult suspend fun update(domain: String, wellknown: String): Result suspend fun delete(domain: String): Result + + fun interface Factory { + fun create(prefix: String?): ElementWellknownStore + } } diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt index f7b73de8e5..830cf4bd4e 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/WellknownRetriever.kt @@ -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 + suspend fun getElementWellKnown( + host: String, + source: EnterpriseRemoteConfigSource + ): WellknownRetrieverResult + + interface Factory { + fun create(getUrlResolver: GetUrlResolver): WellknownRetriever + } } diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStore.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStore.kt index 50c8caeafc..4d58e70db4 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStore.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStore.kt @@ -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 { 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 diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt index 10f668cfb0..df72eddb25 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetriever.kt @@ -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 { + @ContributesBinding(AppScope::class) + @AssistedFactory + fun interface Factory : WellknownRetriever.Factory { + override fun create(getUrlResolver: GetUrlResolver): DefaultWellknownRetriever + } + + override suspend fun getElementWellKnown( + host: String, + source: EnterpriseRemoteConfigSource + ): WellknownRetrieverResult { 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 { - 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 { + 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 Result.toWellknownRetrieverResult(host: String): WellknownRetrieverResult = 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 Throwable.toWellknownRetrieverResult(): WellknownRetrieverResult { + 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 } } }