diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt index f36269abc6..df7a031753 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/DefaultCallWidgetProvider.kt @@ -20,6 +20,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.appconfig.ElementCallConfig import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider @@ -41,9 +42,10 @@ class DefaultCallWidgetProvider @Inject constructor( languageTag: String?, theme: String?, ): Result = runCatching { - val room = matrixClientsProvider.getOrRestore(sessionId).getOrThrow().getRoom(roomId) ?: error("Room not found") + val matrixClient = matrixClientsProvider.getOrRestore(sessionId).getOrThrow() + val room = matrixClient.getRoom(roomId) ?: error("Room not found") val baseUrl = appPreferencesStore.getCustomElementCallBaseUrlFlow().firstOrNull() - ?: elementCallBaseUrlProvider.provides(sessionId) + ?: elementCallBaseUrlProvider.provides(matrixClient) ?: ElementCallConfig.DEFAULT_BASE_URL val widgetSettings = callWidgetSettingsProvider.provide(baseUrl, encrypted = room.isEncrypted) val callUrl = room.generateWidgetWebViewUrl( diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt deleted file mode 100644 index 63eb5208dd..0000000000 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/ElementCallBaseUrlProvider.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2024 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.call.impl.utils - -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.call.impl.wellknown.CallWellknownAPI -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.network.RetrofitFactory -import kotlinx.coroutines.withContext -import retrofit2.HttpException -import timber.log.Timber -import java.net.HttpURLConnection -import javax.inject.Inject - -interface ElementCallBaseUrlProvider { - suspend fun provides(sessionId: SessionId): String? -} - -@SingleIn(AppScope::class) -@ContributesBinding(AppScope::class) -class DefaultElementCallBaseUrlProvider @Inject constructor( - private val retrofitFactory: RetrofitFactory, - private val coroutineDispatchers: CoroutineDispatchers, -) : ElementCallBaseUrlProvider { - private val apiCache = mutableMapOf() - - override suspend fun provides(sessionId: SessionId): String? = withContext(coroutineDispatchers.io) { - val domain = sessionId.value.substringAfter(":") - val callWellknownAPI = apiCache.getOrPut(sessionId) { - retrofitFactory.create("https://$domain") - .create(CallWellknownAPI::class.java) - } - try { - callWellknownAPI.getCallWellKnown().widgetUrl - } catch (e: HttpException) { - // Ignore Http 404, but re-throws any other exceptions - if (e.code() != HttpURLConnection.HTTP_NOT_FOUND) { - throw e - } - Timber.w(e, "Failed to fetch wellknown data") - null - } - } -} diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt index 73c8d3515e..7eb426c35e 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/DefaultCallWidgetProviderTest.kt @@ -18,9 +18,9 @@ package io.element.android.features.call.utils import com.google.common.truth.Truth.assertThat import io.element.android.features.call.impl.utils.DefaultCallWidgetProvider -import io.element.android.features.call.impl.utils.ElementCallBaseUrlProvider +import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider import io.element.android.libraries.matrix.api.widget.CallWidgetSettingsProvider import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID @@ -116,9 +116,9 @@ class DefaultCallWidgetProviderTest { @Test fun `getWidget - will use a wellknown base url if it exists`() = runTest { val aCustomUrl = "https://custom.element.io" - val providesLambda = lambdaRecorder { _ -> aCustomUrl } - val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { sessionId -> - providesLambda(sessionId) + val providesLambda = lambdaRecorder { _ -> aCustomUrl } + val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { matrixClient -> + providesLambda(matrixClient) } val room = FakeMatrixRoom().apply { givenGenerateWidgetWebViewUrlResult(Result.success("url")) @@ -137,7 +137,7 @@ class DefaultCallWidgetProviderTest { assertThat(settingsProvider.providedBaseUrls).containsExactly(aCustomUrl) providesLambda.assertions() .isCalledOnce() - .with(value(A_SESSION_ID)) + .with(value(client)) } private fun createProvider( diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt index 619659e1df..660bb7249f 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/FakeElementCallBaseUrlProvider.kt @@ -16,14 +16,14 @@ package io.element.android.features.call.utils -import io.element.android.features.call.impl.utils.ElementCallBaseUrlProvider -import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider import io.element.android.tests.testutils.lambda.lambdaError class FakeElementCallBaseUrlProvider( - private val providesLambda: (SessionId) -> String? = { lambdaError() } + private val providesLambda: (MatrixClient) -> String? = { lambdaError() } ) : ElementCallBaseUrlProvider { - override suspend fun provides(sessionId: SessionId): String? { - return providesLambda(sessionId) + override suspend fun provides(matrixClient: MatrixClient): String? { + return providesLambda(matrixClient) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 4dd00984f1..b3b47d4499 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -121,4 +121,15 @@ interface MatrixClient : Closeable { * This flow will emit a new value whenever the send queue is disabled for a room. */ fun sendQueueDisabledFlow(): Flow + + /** + * Return the server name part of the current user ID, using the SDK, and if a failure occurs, + * compute it manually. + */ + fun userIdServerName(): String + + /** + * Execute generic GET requests through the SDKs internal HTTP client. + */ + suspend fun getUrl(url: String): Result } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellKnown.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt similarity index 58% rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellKnown.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt index b2e87c907b..94c9cb3489 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellKnown.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/call/ElementCallBaseUrlProvider.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,22 +14,10 @@ * limitations under the License. */ -package io.element.android.features.call.impl.wellknown +package io.element.android.libraries.matrix.api.call -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable +import io.element.android.libraries.matrix.api.MatrixClient -/** - * Example: - *
- * {
- *     "widget_url": "https://call.server.com"
- * }
- * 
- * . - */ -@Serializable -data class CallWellKnown( - @SerialName("widget_url") - val widgetUrl: String? = null, -) +interface ElementCallBaseUrlProvider { + suspend fun provides(matrixClient: MatrixClient): String? +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt index fecac81633..ac77e58986 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt @@ -16,9 +16,6 @@ package io.element.android.libraries.matrix.api.core -import io.element.android.libraries.androidutils.metadata.isInDebug -import timber.log.Timber - /** * This class contains pattern to match the different Matrix ids * Ref: https://matrix.org/docs/spec/appendices#identifier-grammar @@ -31,7 +28,7 @@ object MatrixPatterns { // See https://matrix.org/docs/spec/appendices#historical-user-ids // Sadly, we need to relax the regex pattern a bit as there already exist some ids that don't match the spec. private const val MATRIX_USER_IDENTIFIER_REGEX = "^@.*?$DOMAIN_REGEX$" - val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) + private val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) // regex pattern to find room ids in a string. private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9.-]+$DOMAIN_REGEX" @@ -54,52 +51,6 @@ object MatrixPatterns { private const val MATRIX_EVENT_IDENTIFIER_V4_REGEX = "\\$[A-Z0-9\\-_]+" private val PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4 = MATRIX_EVENT_IDENTIFIER_V4_REGEX.toRegex(RegexOption.IGNORE_CASE) - // regex pattern to find group ids in a string. - private const val MATRIX_GROUP_IDENTIFIER_REGEX = "\\+[A-Z0-9=_\\-./]+$DOMAIN_REGEX" - private val PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER = MATRIX_GROUP_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) - - // regex pattern to find permalink with message id. - // Android does not support in URL so extract it. - private const val PERMALINK_BASE_REGEX = "https://matrix\\.to/#/" - private const val APP_BASE_REGEX = "https://[A-Z0-9.-]+\\.[A-Z]{2,}/[A-Z]{3,}/#/room/" - const val SEP_REGEX = "/" - - private const val LINK_TO_ROOM_ID_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX - private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID = LINK_TO_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE) - - private const val LINK_TO_ROOM_ALIAS_REGEXP = PERMALINK_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX - private val PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS = LINK_TO_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE) - - private const val LINK_TO_APP_ROOM_ID_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_IDENTIFIER_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX - private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID = LINK_TO_APP_ROOM_ID_REGEXP.toRegex(RegexOption.IGNORE_CASE) - - private const val LINK_TO_APP_ROOM_ALIAS_REGEXP = APP_BASE_REGEX + MATRIX_ROOM_ALIAS_REGEX + SEP_REGEX + MATRIX_EVENT_IDENTIFIER_REGEX - private val PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS = LINK_TO_APP_ROOM_ALIAS_REGEXP.toRegex(RegexOption.IGNORE_CASE) - - // ascii characters in the range \x20 (space) to \x7E (~) - val ORDER_STRING_REGEX = "[ -~]+".toRegex() - - // list of patterns to find some matrix item. - val MATRIX_PATTERNS = listOf( - PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ID, - PATTERN_CONTAIN_MATRIX_TO_PERMALINK_ROOM_ALIAS, - PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ID, - PATTERN_CONTAIN_APP_LINK_PERMALINK_ROOM_ALIAS, - PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER, - PATTERN_CONTAIN_MATRIX_ALIAS, - PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER, - PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER, - PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER - ) - - /** - * Tells if a string is a valid session Id. This is an alias for [isUserId] - * - * @param str the string to test - * @return true if the string is a valid session id - */ - fun isSessionId(str: String?) = isUserId(str) - /** * Tells if a string is a valid user Id. * @@ -158,69 +109,4 @@ object MatrixPatterns { * @return true if the string is a valid thread id. */ fun isThreadId(str: String?) = isEventId(str) - - /** - * Tells if a string is a valid group id. - * - * @param str the string to test - * @return true if the string is a valid group id. - */ - fun isGroupId(str: String?): Boolean { - return str != null && str matches PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER - } - - /** - * Extract server name from a matrix id. - * - * @param matrixId - * @return null if not found or if matrixId is null - */ - fun extractServerNameFromId(matrixId: String?): String? { - return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() } - } - - /** - * Extract user name from a matrix id. - * - * @param matrixId - * @return null if the input is not a valid matrixId - */ - fun extractUserNameFromId(matrixId: String): String? { - return if (isUserId(matrixId)) { - matrixId.removePrefix("@").substringBefore(":", missingDelimiterValue = "") - } else { - null - } - } - - /** - * Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7E (~), - * or consist of more than 50 characters, are forbidden and the field should be ignored if received. - */ - fun isValidOrderString(order: String?): Boolean { - return order != null && order.length < 50 && order matches ORDER_STRING_REGEX - } - - /* - fun candidateAliasFromRoomName(roomName: String, domain: String): String { - return roomName.lowercase() - .replaceSpaceChars(replacement = "_") - .removeInvalidRoomNameChars() - .take(MatrixConstants.maxAliasLocalPartLength(domain)) - } - */ - - /** - * Return the domain form a userId. - * Examples: - * - "@alice:domain.org".getDomain() will return "domain.org" - * - "@bob:domain.org:3455".getDomain() will return "domain.org:3455" - */ - fun String.getServerName(): String { - if (isInDebug && !isUserId(this)) { - // They are some invalid userId localpart in the wild, but the domain part should be there anyway - Timber.w("Not a valid user ID: $this") - } - return substringAfter(":") - } } diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellknownAPI.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/server/UserServerResolver.kt similarity index 67% rename from features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellknownAPI.kt rename to libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/server/UserServerResolver.kt index e2b5d0e54f..1de60a98e3 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/wellknown/CallWellknownAPI.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/server/UserServerResolver.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -14,11 +14,8 @@ * limitations under the License. */ -package io.element.android.features.call.impl.wellknown +package io.element.android.libraries.matrix.api.server -import retrofit2.http.GET - -internal interface CallWellknownAPI { - @GET(".well-known/element/call.json") - suspend fun getCallWellKnown(): CallWellKnown +interface UserServerResolver { + fun resolve(): String } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index ebb871541d..c8574a7934 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -280,6 +280,23 @@ class RustMatrixClient( } } + override fun userIdServerName(): String { + return runCatching { + client.userIdServerName() + } + .onFailure { + Timber.w(it, "Failed to get userIdServerName") + } + .getOrNull() + ?: sessionId.value.substringAfter(":") + } + + override suspend fun getUrl(url: String): Result = withContext(sessionDispatcher) { + runCatching { + client.getUrl(url) + } + } + override suspend fun getRoom(roomId: RoomId): MatrixRoom? { return roomFactory.create(roomId) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt new file mode 100644 index 0000000000..debc0e9174 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProvider.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.call + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.call.ElementCallBaseUrlProvider +import timber.log.Timber +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultElementCallBaseUrlProvider @Inject constructor( + private val elementWellKnownParser: ElementWellKnownParser, +) : ElementCallBaseUrlProvider { + override suspend fun provides(matrixClient: MatrixClient): String? { + val url = buildString { + append("https://") + append(matrixClient.userIdServerName()) + append("/.well-known/element/element.json") + } + return matrixClient.getUrl(url) + .onFailure { failure -> + Timber.w(failure, "Failed to fetch well-known element.json") + } + .getOrNull() + ?.let { wellKnownStr -> + elementWellKnownParser.parse(wellKnownStr) + .onFailure { failure -> + // Can be a HTML 404. + Timber.w(failure, "Failed to parse content") + } + .getOrNull() + } + ?.call + ?.widgetUrl + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt new file mode 100644 index 0000000000..aac31b89ff --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/call/ElementWellKnownParser.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.call + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import org.matrix.rustcomponents.sdk.ElementWellKnown +import org.matrix.rustcomponents.sdk.makeElementWellKnown +import javax.inject.Inject + +interface ElementWellKnownParser { + fun parse(str: String): Result +} + +@ContributesBinding(AppScope::class) +class RustElementWellKnownParser @Inject constructor() : ElementWellKnownParser { + override fun parse(str: String): Result { + return runCatching { + makeElementWellKnown(str) + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolver.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolver.kt new file mode 100644 index 0000000000..ac20573324 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/server/DefaultUserServerResolver.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.server + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.server.UserServerResolver +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +class DefaultUserServerResolver @Inject constructor( + private val matrixClient: MatrixClient, +) : UserServerResolver { + override fun resolve(): String { + return matrixClient.userIdServerName() + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt new file mode 100644 index 0000000000..1d6e8fe443 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/DefaultElementCallBaseUrlProviderTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.call + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EXCEPTION +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.rustcomponents.sdk.ElementCallWellKnown +import org.matrix.rustcomponents.sdk.ElementWellKnown + +class DefaultElementCallBaseUrlProviderTest { + @Test + fun `provides returns null when getUrl returns an error`() = runTest { + val userIdServerNameLambda = lambdaRecorder { "example.com" } + val getUrlLambda = lambdaRecorder> { _ -> + Result.failure(AN_EXCEPTION) + } + val sut = DefaultElementCallBaseUrlProvider( + FakeElementWellKnownParser( + Result.success(createElementWellKnown("")) + ) + ) + val matrixClient = FakeMatrixClient( + userIdServerNameLambda = userIdServerNameLambda, + getUrlLambda = getUrlLambda, + ) + val result = sut.provides(matrixClient) + assertThat(result).isNull() + userIdServerNameLambda.assertions().isCalledOnce() + getUrlLambda.assertions().isCalledOnce() + .with(value("https://example.com/.well-known/element/element.json")) + } + + @Test + fun `provides returns null when content parsing fails`() = runTest { + val userIdServerNameLambda = lambdaRecorder { "example.com" } + val getUrlLambda = lambdaRecorder> { _ -> + Result.success("""{"call":{"widget_url":"https://example.com/call"}}""") + } + val sut = DefaultElementCallBaseUrlProvider( + createFakeElementWellKnownParser( + Result.failure(AN_EXCEPTION) + ) + ) + val matrixClient = FakeMatrixClient( + userIdServerNameLambda = userIdServerNameLambda, + getUrlLambda = getUrlLambda, + ) + val result = sut.provides(matrixClient) + assertThat(result).isNull() + userIdServerNameLambda.assertions().isCalledOnce() + getUrlLambda.assertions().isCalledOnce() + .with(value("https://example.com/.well-known/element/element.json")) + } + + @Test + fun `provides returns value when getUrl returns correct content`() = runTest { + val userIdServerNameLambda = lambdaRecorder { "example.com" } + val getUrlLambda = lambdaRecorder> { _ -> + Result.success("""{"call":{"widget_url":"https://example.com/call"}}""") + } + val sut = DefaultElementCallBaseUrlProvider( + createFakeElementWellKnownParser( + Result.success(createElementWellKnown("aUrl")) + ) + ) + val matrixClient = FakeMatrixClient( + userIdServerNameLambda = userIdServerNameLambda, + getUrlLambda = getUrlLambda, + ) + val result = sut.provides(matrixClient) + assertThat(result).isEqualTo("aUrl") + userIdServerNameLambda.assertions().isCalledOnce() + getUrlLambda.assertions().isCalledOnce() + .with(value("https://example.com/.well-known/element/element.json")) + } + + private fun createFakeElementWellKnownParser(result: Result): FakeElementWellKnownParser { + return FakeElementWellKnownParser(result) + } + + private fun createElementWellKnown(widgetUrl: String): ElementWellKnown { + return ElementWellKnown( + call = ElementCallWellKnown( + widgetUrl = widgetUrl + ) + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/FakeElementWellKnownParser.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/FakeElementWellKnownParser.kt new file mode 100644 index 0000000000..d6108b955f --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/call/FakeElementWellKnownParser.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.call + +import org.matrix.rustcomponents.sdk.ElementWellKnown + +class FakeElementWellKnownParser( + private val result: Result +) : ElementWellKnownParser { + override fun parse(str: String): Result { + return result + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index d2eb47a585..1b6223279c 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -82,6 +82,8 @@ class FakeMatrixClient( private val resolveRoomAliasResult: (RoomAlias) -> Result = { Result.success(ResolvedRoomAlias(A_ROOM_ID, emptyList())) }, private val getRoomPreviewFromRoomIdResult: (RoomId, List) -> Result = { _, _ -> Result.failure(AN_EXCEPTION) }, private val clearCacheLambda: () -> Unit = { lambdaError() }, + private val userIdServerNameLambda: () -> String = { lambdaError() }, + private val getUrlLambda: (String) -> Result = { lambdaError() }, ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -313,4 +315,12 @@ class FakeMatrixClient( var sendQueueDisabledFlow = emptyFlow() override fun sendQueueDisabledFlow(): Flow = sendQueueDisabledFlow + + override fun userIdServerName(): String { + return userIdServerNameLambda() + } + + override suspend fun getUrl(url: String): Result { + return getUrlLambda(url) + } }