Skip to content

Commit

Permalink
Merge pull request #3127 from element-hq/feature/bma/elementWellKnown
Browse files Browse the repository at this point in the history
Let the SDK retrieve and parse Element well known content
  • Loading branch information
bmarty authored Jul 2, 2024
2 parents 725bd9e + 3135c53 commit 37eaa5e
Show file tree
Hide file tree
Showing 15 changed files with 318 additions and 214 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,9 +42,10 @@ class DefaultCallWidgetProvider @Inject constructor(
languageTag: String?,
theme: String?,
): Result<CallWidgetProvider.GetWidgetResult> = 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(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<SessionId, String?> { _ -> aCustomUrl }
val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { sessionId ->
providesLambda(sessionId)
val providesLambda = lambdaRecorder<MatrixClient, String?> { _ -> aCustomUrl }
val elementCallBaseUrlProvider = FakeElementCallBaseUrlProvider { matrixClient ->
providesLambda(matrixClient)
}
val room = FakeMatrixRoom().apply {
givenGenerateWidgetWebViewUrlResult(Result.success("url"))
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<RoomId>

/**
* 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<String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
* <pre>
* {
* "widget_url": "https://call.server.com"
* }
* </pre>
* .
*/
@Serializable
data class CallWellKnown(
@SerialName("widget_url")
val widgetUrl: String? = null,
)
interface ElementCallBaseUrlProvider {
suspend fun provides(matrixClient: MatrixClient): String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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.
*
Expand Down Expand Up @@ -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(":")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = withContext(sessionDispatcher) {
runCatching {
client.getUrl(url)
}
}

override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
return roomFactory.create(roomId)
}
Expand Down
Loading

0 comments on commit 37eaa5e

Please sign in to comment.