Skip to content

Parse permalink using parseMatrixEntityFrom from the SDK #2709

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/2709.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Parse permalink using parseMatrixEntityFrom from the SDK
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package io.element.android.features.messages.impl

import android.content.Context
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -108,13 +107,19 @@ class MessagesNode @AssistedInject constructor(
context: Context,
url: String,
) {
when (val permalink = permalinkParser.parse(Uri.parse(url))) {
when (val permalink = permalinkParser.parse(url)) {
is PermalinkData.UserLink -> {
callback?.onUserDataClicked(UserId(permalink.userId))
callback?.onUserDataClicked(permalink.userId)
}
is PermalinkData.RoomLink -> {
// TODO Implement room link handling
}
is PermalinkData.EventIdAliasLink -> {
// TODO Implement room and Event link handling
}
is PermalinkData.EventIdLink -> {
// TODO Implement room and Event link handling
}
is PermalinkData.FallbackLink,
is PermalinkData.RoomEmailInviteLink -> {
context.openUrlInExternalApp(url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package io.element.android.features.messages.impl.messagecomposer

import android.net.Uri
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.messages.impl.mentions.MentionSuggestion
import io.element.android.libraries.matrix.api.core.UserId
Expand Down Expand Up @@ -49,7 +48,6 @@ fun aMessageComposerState(
richTextEditorState = richTextEditorState,
permalinkParser = object : PermalinkParser {
override fun parse(uriString: String): PermalinkData = TODO()
override fun parse(uri: Uri): PermalinkData = TODO()
},
isFullScreen = isFullScreen,
mode = mode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package io.element.android.libraries.matrix.api.permalink

import android.net.Uri
import androidx.compose.runtime.Immutable
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 kotlinx.collections.immutable.ImmutableList

/**
Expand All @@ -27,28 +29,44 @@ import kotlinx.collections.immutable.ImmutableList
*/
@Immutable
sealed interface PermalinkData {
data class RoomLink(
val roomIdOrAlias: String,
val isRoomAlias: Boolean,
val eventId: String?,
sealed interface RoomLink : PermalinkData {
val viaParameters: ImmutableList<String>
) : PermalinkData {
fun getRoomId(): RoomId? {
return roomIdOrAlias.takeIf { !isRoomAlias }?.let(::RoomId)
}
}

data class RoomIdLink(
val roomId: RoomId,
override val viaParameters: ImmutableList<String>
) : RoomLink

data class RoomAliasLink(
val roomAlias: String,
override val viaParameters: ImmutableList<String>
) : RoomLink

fun getRoomAlias(): String? {
return roomIdOrAlias.takeIf { isRoomAlias }
}
sealed interface EventLink : PermalinkData {
val eventId: EventId
val viaParameters: ImmutableList<String>
}

data class EventIdLink(
val roomId: RoomId,
override val eventId: EventId,
override val viaParameters: ImmutableList<String>
) : EventLink

data class EventIdAliasLink(
val roomAlias: String,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create RoomAlias and RoomOrAliasId value classes? I really think it'll be easier to manage like that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, maybe, but I thought you created it?

And I am pretty sure that when we build a permalink for an event, the roomId is used, even if a room Alias is availalbe. The main reason is that roomId are immutable (module room upgrade...), and aliases can change.

Can this be handled separately?

override val eventId: EventId,
override val viaParameters: ImmutableList<String>
) : EventLink

/*
* &room_name=Team2
* &room_avatar_url=mxc:
* &inviter_name=bob
*/
data class RoomEmailInviteLink(
val roomId: String,
val roomId: RoomId,
val email: String,
val signUrl: String,
val roomName: String?,
Expand All @@ -60,7 +78,7 @@ sealed interface PermalinkData {
val roomType: String?
) : PermalinkData

data class UserLink(val userId: String) : PermalinkData
data class UserLink(val userId: UserId) : PermalinkData

data class FallbackLink(val uri: Uri, val isLegacyGroupLink: Boolean = false) : PermalinkData
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

package io.element.android.libraries.matrix.api.permalink

import android.net.Uri

/**
* This class turns a uri to a [PermalinkData].
* element-based domains (e.g. https://app.element.io/#/user/@chagai95:matrix.org) permalinks
Expand All @@ -27,12 +25,7 @@ import android.net.Uri
interface PermalinkParser {
/**
* Turns a uri string to a [PermalinkData].
*/
fun parse(uriString: String): PermalinkData

/**
* Turns a uri to a [PermalinkData].
* https://github.com/matrix-org/matrix-doc/blob/master/proposals/1704-matrix.to-permalinks.md
*/
fun parse(uri: Uri): PermalinkData
fun parse(uriString: String): PermalinkData
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ import io.element.android.libraries.matrix.api.core.UserId
sealed interface Mention {
data class User(val userId: UserId) : Mention
data object AtRoom : Mention
data class Room(val roomId: RoomId?, val roomAlias: String?) : Mention
data class Room(val roomId: RoomId) : Mention
data class RoomAlias(val roomAlias: String?) : Mention
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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()
val result = runCatching {
parseMatrixEntityFrom(matrixToUri.toString())
}.getOrNull()
return if (result == null) {
PermalinkData.FallbackLink(uri)
} else {
val viaParameters = result.via.toImmutableList()
when (val id = result.id) {
is MatrixId.Room -> PermalinkData.RoomIdLink(
roomId = RoomId(id.id),
viaParameters = viaParameters,
)
}
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
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,
)
} 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())
}
}
Loading