Benoit Marty
6 months ago
committed by
Benoit Marty
52 changed files with 771 additions and 318 deletions
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
/* |
||||
* Copyright (c) 2022 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.libraries.matrix.impl.permalink |
||||
|
||||
import android.net.Uri |
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.appconfig.MatrixConfiguration |
||||
import io.element.android.libraries.di.AppScope |
||||
import io.element.android.libraries.matrix.api.permalink.MatrixToConverter |
||||
import javax.inject.Inject |
||||
|
||||
/** |
||||
* Mapping of an input URI to a matrix.to compliant URI. |
||||
*/ |
||||
@ContributesBinding(AppScope::class) |
||||
class DefaultMatrixToConverter @Inject constructor() : MatrixToConverter { |
||||
/** |
||||
* Try to convert a URL from an element web instance or from a client permalink to a matrix.to url. |
||||
* To be successfully converted, URL path should contain one of the [SUPPORTED_PATHS]. |
||||
* Examples: |
||||
* - https://riot.im/develop/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org |
||||
* - https://app.element.io/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org |
||||
* - https://www.example.org/#/room/#element-android:matrix.org -> https://matrix.to/#/#element-android:matrix.org |
||||
*/ |
||||
override fun convert(uri: Uri): Uri? { |
||||
val uriString = uri.toString() |
||||
val baseUrl = MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL |
||||
|
||||
return when { |
||||
// URL is already a matrix.to |
||||
uriString.startsWith(baseUrl) -> uri |
||||
// Web or client url |
||||
SUPPORTED_PATHS.any { it in uriString } -> { |
||||
val path = SUPPORTED_PATHS.first { it in uriString } |
||||
Uri.parse(baseUrl + uriString.substringAfter(path)) |
||||
} |
||||
// URL is not supported |
||||
else -> null |
||||
} |
||||
} |
||||
|
||||
private val SUPPORTED_PATHS = listOf( |
||||
"/#/room/", |
||||
"/#/user/", |
||||
"/#/group/" |
||||
) |
||||
} |
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
/* |
||||
* Copyright (c) 2023 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.libraries.matrix.impl.permalink |
||||
|
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.appconfig.MatrixConfiguration |
||||
import io.element.android.libraries.di.AppScope |
||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns |
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder |
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilderError |
||||
import javax.inject.Inject |
||||
|
||||
@ContributesBinding(AppScope::class) |
||||
class DefaultPermalinkBuilder @Inject constructor() : PermalinkBuilder { |
||||
private val permalinkBaseUrl |
||||
get() = (MatrixConfiguration.clientPermalinkBaseUrl ?: MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL).also { |
||||
var baseUrl = it |
||||
if (!baseUrl.endsWith("/")) { |
||||
baseUrl += "/" |
||||
} |
||||
if (!baseUrl.endsWith("/#/")) { |
||||
baseUrl += "/#/" |
||||
} |
||||
} |
||||
|
||||
override fun permalinkForUser(userId: UserId): Result<String> { |
||||
return if (MatrixPatterns.isUserId(userId.value)) { |
||||
val url = buildString { |
||||
append(permalinkBaseUrl) |
||||
if (!isMatrixTo()) { |
||||
append(USER_PATH) |
||||
} |
||||
append(userId.value) |
||||
} |
||||
Result.success(url) |
||||
} else { |
||||
Result.failure(PermalinkBuilderError.InvalidUserId) |
||||
} |
||||
} |
||||
|
||||
override fun permalinkForRoomAlias(roomAlias: String): Result<String> { |
||||
return if (MatrixPatterns.isRoomAlias(roomAlias)) { |
||||
Result.success(permalinkForRoomAliasOrId(roomAlias)) |
||||
} else { |
||||
Result.failure(PermalinkBuilderError.InvalidRoomAlias) |
||||
} |
||||
} |
||||
|
||||
override fun permalinkForRoomId(roomId: RoomId): Result<String> { |
||||
return if (MatrixPatterns.isRoomId(roomId.value)) { |
||||
Result.success(permalinkForRoomAliasOrId(roomId.value)) |
||||
} else { |
||||
Result.failure(PermalinkBuilderError.InvalidRoomId) |
||||
} |
||||
} |
||||
|
||||
private fun permalinkForRoomAliasOrId(value: String): String { |
||||
val id = escapeId(value) |
||||
return buildString { |
||||
append(permalinkBaseUrl) |
||||
if (!isMatrixTo()) { |
||||
append(ROOM_PATH) |
||||
} |
||||
append(id) |
||||
} |
||||
} |
||||
|
||||
private fun escapeId(value: String) = value.replace("/", "%2F") |
||||
|
||||
private fun isMatrixTo(): Boolean = permalinkBaseUrl.startsWith(MatrixConfiguration.MATRIX_TO_PERMALINK_BASE_URL) |
||||
|
||||
companion object { |
||||
private const val ROOM_PATH = "room/" |
||||
private const val USER_PATH = "user/" |
||||
} |
||||
} |
@ -0,0 +1,158 @@
@@ -0,0 +1,158 @@
|
||||
/* |
||||
* 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.libraries.matrix.impl.permalink |
||||
|
||||
import android.net.Uri |
||||
import android.net.UrlQuerySanitizer |
||||
import com.squareup.anvil.annotations.ContributesBinding |
||||
import io.element.android.libraries.di.AppScope |
||||
import io.element.android.libraries.matrix.api.core.MatrixPatterns |
||||
import io.element.android.libraries.matrix.api.permalink.MatrixToConverter |
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData |
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser |
||||
import kotlinx.collections.immutable.toImmutableList |
||||
import timber.log.Timber |
||||
import java.net.URLDecoder |
||||
import javax.inject.Inject |
||||
|
||||
/** |
||||
* This class turns a uri to a [PermalinkData]. |
||||
* element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks |
||||
* or matrix.to permalinks (e.g. https://matrix.to/#/@chagai95:matrix.org) |
||||
* or client permalinks (e.g. <clientPermalinkBaseUrl>user/@chagai95:matrix.org) |
||||
*/ |
||||
@ContributesBinding(AppScope::class) |
||||
class DefaultPermalinkParser @Inject constructor( |
||||
private val matrixToConverter: MatrixToConverter |
||||
) : PermalinkParser { |
||||
/** |
||||
* Turns a uri string to a [PermalinkData]. |
||||
*/ |
||||
override fun parse(uriString: String): PermalinkData { |
||||
val uri = Uri.parse(uriString) |
||||
return parse(uri) |
||||
} |
||||
|
||||
/** |
||||
* Turns a uri to a [PermalinkData]. |
||||
* https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md |
||||
*/ |
||||
override fun parse(uri: Uri): PermalinkData { |
||||
// the client or element-based domain permalinks (e.g. https://app.element.io/#/user/@chagai95:matrix.org) don't have the |
||||
// mxid in the first param (like matrix.to does - https://matrix.to/#/@chagai95:matrix.org) but rather in the second after /user/ so /user/mxid |
||||
// so convert URI to matrix.to to simplify parsing process |
||||
val matrixToUri = matrixToConverter.convert(uri) ?: return PermalinkData.FallbackLink(uri) |
||||
|
||||
// We can't use uri.fragment as it is decoding to early and it will break the parsing |
||||
// of parameters that represents url (like signurl) |
||||
val fragment = matrixToUri.toString().substringAfter("#") // uri.fragment |
||||
if (fragment.isEmpty()) { |
||||
return PermalinkData.FallbackLink(uri) |
||||
} |
||||
val safeFragment = fragment.substringBefore('?') |
||||
val viaQueryParameters = fragment.getViaParameters() |
||||
|
||||
// we are limiting to 2 params |
||||
val params = safeFragment |
||||
.split(MatrixPatterns.SEP_REGEX) |
||||
.filter { it.isNotEmpty() } |
||||
.take(2) |
||||
|
||||
val decodedParams = params |
||||
.map { URLDecoder.decode(it, "UTF-8") } |
||||
|
||||
val identifier = params.getOrNull(0) |
||||
val decodedIdentifier = decodedParams.getOrNull(0) |
||||
val extraParameter = decodedParams.getOrNull(1) |
||||
return when { |
||||
identifier.isNullOrEmpty() || decodedIdentifier.isNullOrEmpty() -> PermalinkData.FallbackLink(uri) |
||||
MatrixPatterns.isUserId(decodedIdentifier) -> PermalinkData.UserLink(userId = decodedIdentifier) |
||||
MatrixPatterns.isRoomId(decodedIdentifier) -> { |
||||
handleRoomIdCase(fragment, decodedIdentifier, matrixToUri, extraParameter, viaQueryParameters) |
||||
} |
||||
MatrixPatterns.isRoomAlias(decodedIdentifier) -> { |
||||
PermalinkData.RoomLink( |
||||
roomIdOrAlias = decodedIdentifier, |
||||
isRoomAlias = true, |
||||
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, |
||||
viaParameters = viaQueryParameters.toImmutableList() |
||||
) |
||||
} |
||||
else -> PermalinkData.FallbackLink(uri, MatrixPatterns.isGroupId(identifier)) |
||||
} |
||||
} |
||||
|
||||
private fun handleRoomIdCase(fragment: String, identifier: String, uri: Uri, extraParameter: String?, viaQueryParameters: List<String>): PermalinkData { |
||||
// Can't rely on built in parsing because it's messing around the signurl |
||||
val paramList = safeExtractParams(fragment) |
||||
val signUrl = paramList.firstOrNull { it.first == "signurl" }?.second |
||||
val email = paramList.firstOrNull { it.first == "email" }?.second |
||||
return if (signUrl.isNullOrEmpty().not() && email.isNullOrEmpty().not()) { |
||||
try { |
||||
val signValidUri = Uri.parse(signUrl) |
||||
val identityServerHost = signValidUri.authority ?: throw IllegalArgumentException("missing `authority`") |
||||
val token = signValidUri.getQueryParameter("token") ?: throw IllegalArgumentException("missing `token`") |
||||
val privateKey = signValidUri.getQueryParameter("private_key") ?: throw IllegalArgumentException("missing `private_key`") |
||||
PermalinkData.RoomEmailInviteLink( |
||||
roomId = identifier, |
||||
email = email!!, |
||||
signUrl = signUrl!!, |
||||
roomName = paramList.firstOrNull { it.first == "room_name" }?.second, |
||||
inviterName = paramList.firstOrNull { it.first == "inviter_name" }?.second, |
||||
roomAvatarUrl = paramList.firstOrNull { it.first == "room_avatar_url" }?.second, |
||||
roomType = paramList.firstOrNull { it.first == "room_type" }?.second, |
||||
identityServer = identityServerHost, |
||||
token = token, |
||||
privateKey = privateKey |
||||
) |
||||
} catch (failure: Throwable) { |
||||
Timber.i("## Permalink: Failed to parse permalink $signUrl") |
||||
PermalinkData.FallbackLink(uri) |
||||
} |
||||
} else { |
||||
PermalinkData.RoomLink( |
||||
roomIdOrAlias = identifier, |
||||
isRoomAlias = false, |
||||
eventId = extraParameter.takeIf { !it.isNullOrEmpty() && MatrixPatterns.isEventId(it) }, |
||||
viaParameters = viaQueryParameters.toImmutableList() |
||||
) |
||||
} |
||||
} |
||||
|
||||
private fun safeExtractParams(fragment: String) = |
||||
fragment.substringAfter("?").split('&').mapNotNull { |
||||
val splitNameValue = it.split("=") |
||||
if (splitNameValue.size == 2) { |
||||
Pair(splitNameValue[0], URLDecoder.decode(splitNameValue[1], "UTF-8")) |
||||
} else { |
||||
null |
||||
} |
||||
} |
||||
|
||||
private fun String.getViaParameters(): List<String> { |
||||
return runCatching { |
||||
UrlQuerySanitizer(this) |
||||
.parameterList |
||||
.filter { |
||||
it.mParameter == "via" |
||||
} |
||||
.map { |
||||
URLDecoder.decode(it.mValue, "UTF-8") |
||||
} |
||||
}.getOrDefault(emptyList()) |
||||
} |
||||
} |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
/* |
||||
* 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.libraries.matrix.test.permalink |
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder |
||||
|
||||
class FakePermalinkBuilder( |
||||
private val result: () -> Result<String> = { Result.failure(Exception("Not implemented")) } |
||||
) : PermalinkBuilder { |
||||
override fun permalinkForUser(userId: UserId): Result<String> { |
||||
return result() |
||||
} |
||||
|
||||
override fun permalinkForRoomAlias(roomAlias: String): Result<String> { |
||||
return result() |
||||
} |
||||
|
||||
override fun permalinkForRoomId(roomId: RoomId): Result<String> { |
||||
return result() |
||||
} |
||||
} |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
/* |
||||
* 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.libraries.matrix.test.permalink |
||||
|
||||
import android.net.Uri |
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkData |
||||
import io.element.android.libraries.matrix.api.permalink.PermalinkParser |
||||
|
||||
class FakePermalinkParser( |
||||
private var result: () -> PermalinkData = { throw Exception("Not implemented") } |
||||
) : PermalinkParser { |
||||
fun givenResult(result: PermalinkData) { |
||||
this.result = { result } |
||||
} |
||||
|
||||
override fun parse(uriString: String): PermalinkData { |
||||
return result() |
||||
} |
||||
|
||||
override fun parse(uri: Uri): PermalinkData { |
||||
TODO("Not yet implemented") |
||||
} |
||||
} |
Loading…
Reference in new issue