Parse permalink using parseMatrixEntityFrom.

Create new PermalinkData type for link to Events.
Keep matrixToConverter for now to first convert to matrix.to link. At some point it may be done by the SDK.
Remove parse(Uri)
This commit is contained in:
Benoit Marty 2024-04-15 17:46:06 +02:00
parent 89d2f43b1a
commit 3df328b1ab
14 changed files with 125 additions and 241 deletions

View file

@ -17,16 +17,17 @@
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.core.EventId
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.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 org.matrix.rustcomponents.sdk.MatrixId
import org.matrix.rustcomponents.sdk.parseMatrixEntityFrom
import javax.inject.Inject
/**
@ -41,118 +42,45 @@ class DefaultPermalinkParser @Inject constructor(
) : PermalinkParser {
/**
* Turns a uri string to a [PermalinkData].
* https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md
*/
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)
}
val result = runCatching {
parseMatrixEntityFrom(matrixToUri.toString())
}.getOrNull()
return if (result == null) {
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
val viaParameters = result.via.toImmutableList()
when (val id = result.id) {
is MatrixId.Room -> PermalinkData.RoomIdLink(
roomId = RoomId(id.id),
viaParameters = viaParameters,
)
is MatrixId.User -> PermalinkData.UserLink(
userId = UserId(id.id),
)
is MatrixId.RoomAlias -> PermalinkData.RoomAliasLink(
roomAlias = id.alias,
viaParameters = viaParameters,
)
is MatrixId.EventOnRoomId -> PermalinkData.EventIdLink(
roomId = RoomId(id.roomId),
eventId = EventId(id.eventId),
viaParameters = viaParameters,
)
is MatrixId.EventOnRoomAlias -> PermalinkData.EventIdAliasLink(
roomAlias = id.alias,
eventId = EventId(id.eventId),
viaParameters = viaParameters,
)
}
}
private fun String.getViaParameters(): List<String> {
return runCatching {
UrlQuerySanitizer(this)
.parameterList
.filter {
it.mParameter == "via"
}
.map {
URLDecoder.decode(it.mValue, "UTF-8")
}
}.getOrDefault(emptyList())
}
}

View file

@ -17,6 +17,9 @@
package io.element.android.libraries.matrix.impl.permalink
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId
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.PermalinkData
import kotlinx.collections.immutable.persistentListOf
import org.junit.Test
@ -69,7 +72,7 @@ class DefaultPermalinkParserTest {
val url = "https://app.element.io/#/user/@test:matrix.org"
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.UserLink(
userId = "@test:matrix.org"
userId = UserId("@test:matrix.org")
)
)
}
@ -81,10 +84,8 @@ class DefaultPermalinkParserTest {
)
val url = "https://app.element.io/#/room/!aBCD1234:matrix.org"
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomLink(
roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false,
eventId = null,
PermalinkData.RoomIdLink(
roomId = RoomId("!aBCD1234:matrix.org"),
viaParameters = persistentListOf(),
)
)
@ -97,10 +98,9 @@ class DefaultPermalinkParserTest {
)
val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/$1234567890abcdef:matrix.org"
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomLink(
roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false,
eventId = "\$1234567890abcdef:matrix.org",
PermalinkData.EventIdLink(
roomId = RoomId("!aBCD1234:matrix.org"),
eventId = EventId("$1234567890abcdef:matrix.org"),
viaParameters = persistentListOf(),
)
)
@ -113,10 +113,8 @@ class DefaultPermalinkParserTest {
)
val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/1234567890abcdef:matrix.org"
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomLink(
roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false,
eventId = null,
PermalinkData.RoomIdLink(
roomId = RoomId("!aBCD1234:matrix.org"),
viaParameters = persistentListOf(),
)
)
@ -129,10 +127,9 @@ class DefaultPermalinkParserTest {
)
val url = "https://app.element.io/#/room/!aBCD1234:matrix.org/$1234567890abcdef:matrix.org?via=matrix.org&via=matrix.com"
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomLink(
roomIdOrAlias = "!aBCD1234:matrix.org",
isRoomAlias = false,
eventId = "\$1234567890abcdef:matrix.org",
PermalinkData.EventIdLink(
roomId = RoomId("!aBCD1234:matrix.org"),
eventId = EventId("$1234567890abcdef:matrix.org"),
viaParameters = persistentListOf("matrix.org", "matrix.com"),
)
)
@ -145,10 +142,23 @@ class DefaultPermalinkParserTest {
)
val url = "https://app.element.io/#/room/#element-android:matrix.org"
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomLink(
roomIdOrAlias = "#element-android:matrix.org",
isRoomAlias = true,
eventId = null,
PermalinkData.RoomAliasLink(
roomAlias = "#element-android:matrix.org",
viaParameters = persistentListOf(),
)
)
}
@Test
fun `parsing a valid room alias with eventId url returns a room link`() {
val sut = DefaultPermalinkParser(
matrixToConverter = DefaultMatrixToConverter(),
)
val url = "https://app.element.io/#/room/#element-android:matrix.org/$1234567890abcdef:matrix.org"
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.EventIdAliasLink(
roomAlias = "#element-android:matrix.org",
eventId = EventId("$1234567890abcdef:matrix.org"),
viaParameters = persistentListOf(),
)
)
@ -188,7 +198,7 @@ class DefaultPermalinkParserTest {
"&room_type="
assertThat(sut.parse(url)).isEqualTo(
PermalinkData.RoomEmailInviteLink(
roomId = "!aBCDEF12345:matrix.org",
roomId = RoomId("!aBCDEF12345:matrix.org"),
email = "testuser@element.io",
signUrl = "https://vector.im/_matrix/identity/api/v1/sign-ed25519?token=a_token&private_key=a_private_key",
roomName = "TestRoom",