From 38f3eaf4cd7f91028513427b5286207e40a84050 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 08:50:33 +0200 Subject: [PATCH 01/12] Revert "Refactor Mentions" This reverts commits f8be363cade6790dbad12e60152bbe71cd2a625a and 8cec75c53dd1a5f778aaa0d62f30f2639cb81f42. --- .../net/folivo/trixnity/core/MatrixRegex.kt | 112 +++ .../net/folivo/trixnity/core/model/EventId.kt | 5 - .../net/folivo/trixnity/core/model/Mention.kt | 47 ++ .../folivo/trixnity/core/model/RoomAliasId.kt | 5 - .../net/folivo/trixnity/core/model/RoomId.kt | 14 +- .../net/folivo/trixnity/core/model/UserId.kt | 5 - .../trixnity/core/util/MatrixIdRegex.kt | 28 - .../folivo/trixnity/core/util/MatrixLinks.kt | 99 +++ .../folivo/trixnity/core/util/References.kt | 225 ----- .../folivo/trixnity/core/MatrixLinkTest.kt | 456 ++++++++++ .../folivo/trixnity/core/MatrixRegexTest.kt | 673 +++++++++++++++ .../trixnity/core/util/ReferencesTest.kt | 786 ------------------ 12 files changed, 1397 insertions(+), 1058 deletions(-) create mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt create mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/Mention.kt delete mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt create mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt delete mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt create mode 100644 trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt create mode 100644 trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt delete mode 100644 trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/ReferencesTest.kt diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt new file mode 100644 index 000000000..51c355407 --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt @@ -0,0 +1,112 @@ +package net.folivo.trixnity.core + +import io.github.oshai.kotlinlogging.KotlinLogging +import net.folivo.trixnity.core.model.Mention +import net.folivo.trixnity.core.model.RoomAliasId +import net.folivo.trixnity.core.model.UserId +import net.folivo.trixnity.core.util.MatrixLinks +import net.folivo.trixnity.core.util.Patterns + +private val log = KotlinLogging.logger {} + +object MatrixRegex { + // language=Regexp + private const val ID_PATTERN = """[@#][0-9a-z\-.=_/+]+:(?:[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|\[[0-9a-fA-F:.]{2,45}\]|[0-9a-zA-Z\-.]{1,255})(?::[0-9]{1,5})?""" + private val idRegex = ID_PATTERN.toRegex() + + fun findMentions(message: String): Map { + val links = findLinkMentions(message) + val users = findIdMentions(message) + val linksRange = links.keys.sortedBy { it.first } + val uniqueUsers = users.filter { (user, _) -> + // We don't want id matches that overlap with link matches, + // as matrix.to urls will match both as link and as id + !linksRange.overlaps(user) + } + return links.plus(uniqueUsers).toMap() + } + + fun findIdMentions(content: String, from: Int = 0, to: Int = content.length): Map { + return idRegex + .findAll(content, startIndex = from) + .filter { it.range.last < to } + .filter { it.range.last - it.range.first <= 255 } + .mapNotNull { Pair(it.range, parseMatrixId(it.value) ?: return@mapNotNull null) } + .toMap() + } + + fun findLinkMentions(content: String, from: Int = 0, to: Int = content.length): Map { + return Patterns.AUTOLINK_MATRIX_URI + .findAll(content, startIndex = from) + .filter { it.range.last < to } + .mapNotNull { + val trimmedContent = it.value.trimLink() + Pair( + it.range.first.until(it.range.first + trimmedContent.length), + MatrixLinks.parse(trimmedContent) ?: return@mapNotNull null + ) + }.toMap() + } + + fun findLinks(content: String, from: Int = 0, to: Int = content.length): Map { + return Patterns.AUTOLINK_MATRIX_URI + .findAll(content, startIndex = from) + .filter { it.range.last < to } + .map { + val trimmedContent = it.value.trimLink() + Pair( + it.range.first.until(it.range.first + trimmedContent.length), + trimmedContent, + ) + }.toMap() + } + + fun isValidUserId(id: String): Boolean = + id.length <= 255 + && id.startsWith(UserId.sigilCharacter) + && id.matches(idRegex) + + fun isValidRoomAliasId(id: String): Boolean = + id.length <= 255 + && id.startsWith(RoomAliasId.sigilCharacter) + && id.matches(idRegex) + + private fun parseMatrixId(id: String): Mention? { + return when { + id.length > 255 -> { + log.trace { "malformed matrix id: id too long: ${id.length} (max length: 255)" } + null + } + id.startsWith(UserId.sigilCharacter) -> Mention.User(UserId(id)) + id.startsWith(RoomAliasId.sigilCharacter) -> Mention.RoomAlias(RoomAliasId(id)) + else -> null + } + } + + private fun List.overlaps(user: IntRange): Boolean { + val index = binarySearch { link -> + when { + user.last < link.first -> 1 + user.first > link.last -> -1 + user.first >= link.first && user.last <= link.last -> 0 + else -> -1 + } + } + return index >= 0 + } + + private fun String.trimParens(): String = + if (endsWith(')')) { + val trimmed = trimEnd(')') + val openingParens = trimmed.count { it == '(' } + val closingParens = trimmed.count { it == ')' } + val endingParens = length - trimmed.length + val openParens = openingParens - closingParens + + val desiredParens = minOf(endingParens, openParens) + take(trimmed.length + desiredParens) + } else this + + private fun String.trimLink(): String = + trimEnd(',', '.', '!', '?', ':').trimParens() +} \ No newline at end of file diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/EventId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/EventId.kt index 254db46ba..3519c0562 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/EventId.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/EventId.kt @@ -7,18 +7,13 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import net.folivo.trixnity.core.util.MatrixIdRegex @Serializable(with = EventIdSerializer::class) data class EventId(val full: String) { companion object { const val sigilCharacter = '$' - - fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.eventIdRegex) } - val isValid by lazy { isValid(full) } - override fun toString() = full } diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/Mention.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/Mention.kt new file mode 100644 index 000000000..1ffecedfd --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/Mention.kt @@ -0,0 +1,47 @@ +package net.folivo.trixnity.core.model + +import io.ktor.http.* + +/** + * Represents a mention. A mention can refer to various entities and potentially include actions associated with them. + */ +sealed interface Mention { + /** + * If exists, the parameters provided in the URI + */ + val parameters: Parameters? + + + /** + * Represents a mention of a user. + */ + data class User( + val userId: UserId, + override val parameters: Parameters? = parametersOf() + ) : Mention + + /** + * Represents a mention of a room. + */ + data class Room( + val roomId: RoomId, + override val parameters: Parameters? = parametersOf() + ) : Mention + + /** + * Represents a mention of a room alias + */ + data class RoomAlias( + val roomAliasId: RoomAliasId, + override val parameters: Parameters? = parametersOf() + ) : Mention + + /** + * Represents a mention of a generic event. + */ + data class Event( + val roomId: RoomId? = null, + val eventId: EventId, + override val parameters: Parameters? = parametersOf() + ) : Mention +} \ No newline at end of file diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomAliasId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomAliasId.kt index d06989c6b..b610887bf 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomAliasId.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomAliasId.kt @@ -7,7 +7,6 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import net.folivo.trixnity.core.util.MatrixIdRegex @Serializable(with = RoomAliasIdSerializer::class) data class RoomAliasId(val full: String) { @@ -16,8 +15,6 @@ data class RoomAliasId(val full: String) { companion object { const val sigilCharacter = '#' - - fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.roomAliasIdRegex) } val localpart: String @@ -25,8 +22,6 @@ data class RoomAliasId(val full: String) { val domain: String get() = full.trimStart(sigilCharacter).substringAfter(':') - val isValid by lazy { isValid(full) } - override fun toString() = full } diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomId.kt index 34c4b3f73..9cbf470ac 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomId.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomId.kt @@ -7,18 +7,24 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import net.folivo.trixnity.core.util.MatrixIdRegex @Serializable(with = RoomIdSerializer::class) data class RoomId(val full: String) { + @Deprecated("RoomId should be considered as opaque String") + constructor(localpart: String, domain: String) : this("${sigilCharacter}$localpart:$domain") + companion object { const val sigilCharacter = '!' - - fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.roomIdRegex) } - val isValid by lazy { isValid(full) } + @Deprecated("RoomId should be considered as opaque String") + val localpart: String + get() = full.trimStart(sigilCharacter).substringBefore(':') + + @Deprecated("RoomId should be considered as opaque String") + val domain: String + get() = full.trimStart(sigilCharacter).substringAfter(':') override fun toString() = full } diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/UserId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/UserId.kt index dcd752936..5924e7ae2 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/UserId.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/UserId.kt @@ -7,7 +7,6 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -import net.folivo.trixnity.core.util.MatrixIdRegex @Serializable(with = UserIdSerializer::class) data class UserId(val full: String) { @@ -16,8 +15,6 @@ data class UserId(val full: String) { companion object { const val sigilCharacter = '@' - - fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.userIdRegex) } val localpart: String @@ -25,8 +22,6 @@ data class UserId(val full: String) { val domain: String get() = full.trimStart(sigilCharacter).substringAfter(':') - val isValid by lazy { isValid(full) } - override fun toString() = full } diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt deleted file mode 100644 index 71eaaa05e..000000000 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.folivo.trixnity.core.util - -internal object MatrixIdRegex { - // https://spec.matrix.org/v1.14/appendices/#server-name - private const val baseDnsRegex = """(?:[\w.-]{1,230})""" - private const val baseIPV4Regex = """\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}""" - private const val baseIPV6Regex = """\[[0-9a-fA-F:]+\]""" - private const val basePortRegex = """:[0-9]{1,5}""" - private const val servernameRegex = - """(?:(?:(?:$baseDnsRegex)|$baseIPV4Regex)|(?:$baseIPV6Regex))(?:$basePortRegex)?""" - - // https://spec.matrix.org/v1.14/appendices/#opaque-identifiers - private val opaqueIdRegex = """(?:[0-9A-Za-z-._~]+)""" - - // https://spec.matrix.org/v1.14/appendices/#user-identifiers - private val userLocalpartRegex = """(?:[0-9a-z-=_/+.]+)""" - val userIdRegex by lazy { """@($userLocalpartRegex):($servernameRegex)""".toRegex() } - - // https://spec.matrix.org/v1.14/appendices/#room-ids - val roomIdRegex by lazy { """!($opaqueIdRegex)(?::($servernameRegex))?""".toRegex() } - - // https://spec.matrix.org/v1.11/appendices/#room-aliases - private val roomAliasLocalpartRegex = """(?:[^:\s/]+)""" - val roomAliasIdRegex by lazy { """#($roomAliasLocalpartRegex):($servernameRegex)""".toRegex() } - - // https://spec.matrix.org/v1.11/appendices/#event-ids - val eventIdRegex by lazy { """\$($opaqueIdRegex(?::$servernameRegex)?)""".toRegex() } -} \ No newline at end of file diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt new file mode 100644 index 000000000..fc9113ea7 --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt @@ -0,0 +1,99 @@ +package net.folivo.trixnity.core.util + +import io.github.oshai.kotlinlogging.KotlinLogging +import io.ktor.http.* +import net.folivo.trixnity.core.model.EventId +import net.folivo.trixnity.core.model.Mention +import net.folivo.trixnity.core.model.RoomAliasId +import net.folivo.trixnity.core.model.RoomId +import net.folivo.trixnity.core.model.UserId + +private val log = KotlinLogging.logger {} + +object MatrixLinks { + private val matrixProtocol = URLProtocol("matrix", 0) + + fun parse(href: String): Mention? { + val url = Url(href) + if (url.protocol == matrixProtocol) { + return parseMatrixProtocol(url.segments, url.parameters) + } + // matrix.to URLs look like this: + // https://matrix.to/#/!roomId?via=example.org + // protocol=https host=matrix.to segments=[] fragment=/!roomId?via=example.org + if (url.protocol == URLProtocol.HTTPS && url.host == "matrix.to" && url.segments.isEmpty()) { + // matrix.to uses AJAX hash routing, where the entire path is passed within the hash fragment to prevent + // the server from seeing the roomId. + // This means we have to parse this hash back into path segments and query parameters + val path = url.fragment.substringBefore('?').removePrefix("/") + val query = url.fragment.substringAfter('?', missingDelimiterValue = "") + val segments = path.removePrefix("/").split('/') + val parameters = parseQueryString(query, decode = false) + return parseMatrixTo(segments, parameters) + } + return null + } + + private fun parseMatrixTo(path: List, parameters: Parameters): Mention? { + val parts = path.map { id -> + when { + id.length > 255 -> { + log.trace { "malformed matrix link: id too long: ${id.length} (max length: 255)" } + return null + } + id.startsWith(RoomAliasId.sigilCharacter) -> RoomAliasId(id) + id.startsWith(RoomId.sigilCharacter) -> RoomId(id) + id.startsWith(UserId.sigilCharacter) -> UserId(id) + id.startsWith(EventId.sigilCharacter) -> EventId(id) + else -> { + log.trace { "malformed matrix link: invalid id type: ${id.firstOrNull()} (known types: #, !, @, $)" } + null + } + } + } + val first = parts.getOrNull(0) + val second = parts.getOrNull(1) + return when { + first is UserId -> Mention.User(first, parameters) + first is RoomAliasId -> Mention.RoomAlias(first, parameters) + first is EventId -> Mention.Event(null, first, parameters) + first is RoomId && second is EventId -> Mention.Event(first, second, parameters) + first is RoomId -> Mention.Room(first, parameters) + else -> { + log.trace { "malformed matrix link: unknown format" } + null + } + } + } + + private fun parseMatrixProtocol(path: List, parameters: Parameters): Mention? { + val parts = path.windowed(2, 2).map { (type, id) -> + when { + id.length > 255 -> { + log.trace { "malformed matrix link: id too long: ${id.length} (max length: 255)" } + return null + } + type == "roomid" -> RoomId("!$id") + type == "r" -> RoomAliasId("#$id") + type == "u" -> UserId("@$id") + type == "e" -> EventId("$$id") + else -> { + log.trace { "malformed matrix link: invalid id type: $type (known types: roomid, r, u, e)" } + null + } + } + } + val first = parts.getOrNull(0) + val second = parts.getOrNull(1) + return when { + first is UserId -> Mention.User(first, parameters) + first is RoomAliasId -> Mention.RoomAlias(first, parameters) + first is RoomId && second is EventId -> Mention.Event(first, second, parameters) + first is RoomId -> Mention.Room(first, parameters) + else -> { + log.trace { "malformed matrix link: unknown format" } + null + } + } + } +} \ No newline at end of file diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt deleted file mode 100644 index 732bacd33..000000000 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt +++ /dev/null @@ -1,225 +0,0 @@ -package net.folivo.trixnity.core.util - -import io.github.oshai.kotlinlogging.KotlinLogging -import io.ktor.http.* -import net.folivo.trixnity.core.model.EventId -import net.folivo.trixnity.core.model.RoomAliasId -import net.folivo.trixnity.core.model.RoomId -import net.folivo.trixnity.core.model.UserId - -private val log = KotlinLogging.logger("net.folivo.trixnity.core.util.References") - -/** - * Represents a reference. A reference can refer to various entities and potentially include actions associated with them. - */ -sealed interface Reference { - /** - * If exists, the original uri. - */ - val uri: String? - - - /** - * Represents a mention of a user. - */ - data class User( - val userId: UserId, - override val uri: String? = null - ) : Reference - - /** - * Represents a mention of a room. - */ - data class Room( - val roomId: RoomId, - override val uri: String? = null - ) : Reference - - /** - * Represents a mention of a room alias. - */ - data class RoomAlias( - val roomAliasId: RoomAliasId, - override val uri: String? = null - ) : Reference - - /** - * Represents a mention of a generic event. - */ - data class Event( - val roomId: RoomId? = null, - val eventId: EventId, - override val uri: String? = null - ) : Reference - - /** - * Represents a classic link (url). - */ - data class Link( - override val uri: String - ) : Reference -} - -object References { - private val matrixProtocol = URLProtocol("matrix", 0) - - fun findReferences(message: String): Map { - val linkReferences = findLinkReferences(message) - val idReferences = findIdReferences(message) - - val idRanges = idReferences.keys.sortedBy { it.first } - val uniqueLinkReferences = linkReferences.filter { (linkRange, linkReference) -> - // We don't want links matches that overlap with user matches, - // as a matrixIds will match both as link and as id - if (linkReference is Reference.Link) !idRanges.overlaps(linkRange) - else true - } - - val linkRanges = linkReferences.filterValues { it !is Reference.Link }.keys.sortedBy { it.first } - val uniqueIdReferences = idReferences.filter { (userRange, _) -> - // We don't want id matches that overlap with link matches, - // as matrix.to urls will match both as link and as id - !linkRanges.overlaps(userRange) - } - return uniqueIdReferences + uniqueLinkReferences - } - - fun findIdReferences(content: String): Map { - return MatrixIdRegex.userIdRegex.findAll(content).toReferences() + - MatrixIdRegex.roomAliasIdRegex.findAll(content).toReferences() - } - - private fun Sequence.toReferences() = - filter { it.range.last - it.range.first <= 255 } - .mapNotNull { - Pair( - it.range, - when (val matrixId = it.value.toMatrixId()) { - null -> null - is UserId -> Reference.User(matrixId) - is RoomAliasId -> Reference.RoomAlias(matrixId) - is EventId -> Reference.Event(null, matrixId) - is RoomId -> Reference.Room(matrixId) - else -> null - } ?: return@mapNotNull null - ) - } - .toMap() - - fun findLinkReferences(content: String): Map { - return Patterns.AUTOLINK_MATRIX_URI - .findAll(content) - .mapNotNull { - val trimmedContent = it.value.trimLink() - Pair( - it.range.first.until(it.range.first + trimmedContent.length), - parseLink(trimmedContent) ?: return@mapNotNull null - ) - }.toMap() - } - - private fun parseLink(uri: String): Reference? { - val url = try { - Url(uri) - } catch (_: URLParserException) { - null - } - return when { - url == null -> Reference.Link(uri) - url.protocol == matrixProtocol -> parseMatrixProtocol(url, uri) - // matrix.to URLs look like this: - // https://matrix.to/#/!roomId?via=example.org - // protocol=https host=matrix.to segments=[] fragment=/!roomId?via=example.org - url.protocol == URLProtocol.HTTPS && url.host == "matrix.to" && url.segments.isEmpty() -> { - // matrix.to uses AJAX hash routing, where the entire path is passed within the hash fragment to prevent - // the server from seeing the roomId. - // This means we have to parse this hash back into path segments and query parameters - val path = url.fragment.substringBefore('?').removePrefix("/") - val segments = path.removePrefix("/").split('/') - return parseMatrixTo(segments, uri) - } - - else -> Reference.Link(uri) - } - } - - private fun String.toMatrixId() = - when { - startsWith(UserId.sigilCharacter) && UserId.isValid(this) -> UserId(this) - startsWith(RoomAliasId.sigilCharacter) && RoomAliasId.isValid(this) -> RoomAliasId(this) - startsWith(RoomId.sigilCharacter) && RoomId.isValid(this) -> RoomId(this) - startsWith(EventId.sigilCharacter) && EventId.isValid(this) -> EventId(this) - else -> { - log.trace { "malformed matrix id $this invalid id type: ${firstOrNull()} (known types: #, !, @, $)" } - null - } - } - - private fun parseMatrixTo(path: List, uri: String): Reference? { - val parts = path.map { it.toMatrixId() } - val first = parts.getOrNull(0) - val second = parts.getOrNull(1) - return when { - first is UserId -> Reference.User(first, uri) - first is RoomAliasId -> Reference.RoomAlias(first, uri) - first is EventId -> Reference.Event(null, first, uri) - first is RoomId && second is EventId -> Reference.Event(first, second, uri) - first is RoomId -> Reference.Room(first, uri) - else -> { - log.trace { "malformed matrix link $path: unknown format" } - null - } - } - } - - private fun parseMatrixProtocol(url: Url, uri: String): Reference? { - val parts = url.segments.windowed(2, 2).map { (type, id) -> - when (type) { - "roomid" -> "!$id".toMatrixId() - "r" -> "#$id".toMatrixId() - "u" -> "@$id".toMatrixId() - "e" -> "$$id".toMatrixId() - else -> null - } - } - val first = parts.getOrNull(0) - val second = parts.getOrNull(1) - return when { - first is UserId -> Reference.User(first, uri) - first is RoomAliasId -> Reference.RoomAlias(first, uri) - first is RoomId && second is EventId -> Reference.Event(first, second, uri) - first is RoomId -> Reference.Room(first, uri) - else -> { - log.trace { "malformed matrix link: unknown format" } - null - } - } - } - - private fun List.overlaps(user: IntRange): Boolean { - val index = binarySearch { link -> - when { - user.last < link.first -> 1 - user.first > link.last -> -1 - user.first >= link.first && user.last <= link.last -> 0 - else -> -1 - } - } - return index >= 0 - } - - private fun String.trimParens(): String = - if (endsWith(')')) { - val trimmed = trimEnd(')') - val openingParens = trimmed.count { it == '(' } - val closingParens = trimmed.count { it == ')' } - val endingParens = length - trimmed.length - val openParens = openingParens - closingParens - - val desiredParens = minOf(endingParens, openParens) - take(trimmed.length + desiredParens) - } else this - - private fun String.trimLink(): String = - trimEnd(',', '.', '!', '?', ':').trimParens() -} \ No newline at end of file diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt new file mode 100644 index 000000000..fda231702 --- /dev/null +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt @@ -0,0 +1,456 @@ +package net.folivo.trixnity.core + +import io.ktor.http.* +import net.folivo.trixnity.core.model.EventId +import net.folivo.trixnity.core.model.Mention +import net.folivo.trixnity.core.model.RoomAliasId +import net.folivo.trixnity.core.model.RoomId +import net.folivo.trixnity.core.model.UserId +import net.folivo.trixnity.core.util.MatrixLinks +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class MatrixLinkTest { + @Test + fun `fails on invalid links`() { + assertNull(MatrixLinks.parse("invalid-link")) + assertNull(MatrixLinks.parse("https://example.com")) + assertNull(MatrixLinks.parse("https://matrix.to/#/disclaimer/")) + assertNull(MatrixLinks.parse("https://matrix.to/robots.txt")) + assertNull(MatrixLinks.parse("matrix:group/group:example.com")) + } + + @Test + fun `parses matrixto user links`() { + assertEquals( + expected = Mention.User(UserId("@user:example.com")), + actual = MatrixLinks.parse("https://matrix.to/#/@user:example.com") + ) + assertEquals( + expected = Mention.User(UserId("@user:example.com")), + actual = MatrixLinks.parse("https://matrix.to/#%2F%40user%3Aexample.com") + ) + assertEquals( + expected = Mention.User(UserId("@user:example.com"), parametersOf("action", "chat")), + actual = MatrixLinks.parse("https://matrix.to/#/@user:example.com?action=chat") + ) + assertEquals( + expected = Mention.User(UserId("@user:example.com"), parametersOf("action", "chat")), + actual = MatrixLinks.parse("https://matrix.to/#%2F%40user%3Aexample.com%3Faction=chat") + ) + } + + @Test + fun `parses matrix protocol user links`() { + assertEquals( + expected = Mention.User(UserId("@user:example.com")), + actual = MatrixLinks.parse("matrix:u/user:example.com") + ) + assertEquals( + expected = Mention.User(UserId("@user:example.com"), parametersOf("action", "chat")), + actual = MatrixLinks.parse("matrix:u/user:example.com?action=chat") + ) + } + + @Test + fun `parses matrixto room alias links`() { + assertEquals( + expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org")), + actual = MatrixLinks.parse("https://matrix.to/#/#somewhere:example.org") + ) + assertEquals( + expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org")), + actual = MatrixLinks.parse("https://matrix.to/#%2F#somewhere%3Aexample.org") + ) + assertEquals( + expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#/#somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") + ) + assertEquals( + expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#%2F#somewhere%3Aexample.org%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + ) + } + + @Test + fun `parses matrix protocol room alias links`() { + assertEquals( + expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org")), + actual = MatrixLinks.parse("matrix:r/somewhere:example.org") + ) + assertEquals( + expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("matrix:r/somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") + ) + } + + @Test + fun `parses matrixto roomid links`() { + assertEquals( + expected = Mention.Room(RoomId("!somewhere:example.org")), + actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org") + ) + assertEquals( + expected = Mention.Room(RoomId("!somewhere:example.org")), + actual = MatrixLinks.parse("https://matrix.to/#%2F!somewhere%3Aexample.org") + ) + assertEquals( + expected = Mention.Room(RoomId("!somewhere:example.org"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") + ) + assertEquals( + expected = Mention.Room(RoomId("!somewhere:example.org"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#%2F!somewhere%3Aexample.org%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + ) + } + + @Test + fun `parses matrix protocol roomid links`() { + assertEquals( + expected = Mention.Room(RoomId("!somewhere:example.org")), + actual = MatrixLinks.parse("matrix:roomid/somewhere:example.org") + ) + assertEquals( + expected = Mention.Room(RoomId("!somewhere:example.org"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("matrix:roomid/somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") + ) + } + + @Test + fun `parses matrixto roomid v12 links`() { + assertEquals( + expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + ) + assertEquals( + expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + ) + } + + @Test + fun `parses matrix protocol roomid v12 links`() { + assertEquals( + expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + ) + } + + @Test + fun `parses matrixto event links`() { + assertEquals( + expected = Mention.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#/!somewhere%3Aexample.org/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(RoomId("!somewhere:example.org"),EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + ) + assertEquals( + expected = Mention.Event(RoomId("!somewhere:example.org"),EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#%2F!somewhere%3Aexample.org%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + ) + assertEquals( + expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + ) + assertEquals( + expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + ) + } + + @Test + fun `parses matrix protocol event links`() { + assertEquals( + expected = Mention.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("matrix:roomid/somewhere:example.org/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("matrix:roomid/somewhere:example.org/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + ) + } + + @Test + fun `parses matrixto event v12 links`() { + assertEquals( + expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + ) + assertEquals( + expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + ) + assertEquals( + expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + ) + assertEquals( + expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("https://matrix.to/#%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + ) + } + + @Test + fun `parses matrix protocol event v12 links`() { + assertEquals( + expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + "action" to listOf("join"), + "via" to listOf("example.org", "elsewhere.ca") + )), + actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + ) + } + + @Test + fun `allows long matrixto links`() { + val longId = ( + "aaaaaaaaa1aaaaaaaaa2aaaaaaaaa3aaaaaaaaa4aaaaaaaaa5aaaaaaaaa6aaaaaaaaa7aaaaaaaaa8aaaaaaaaa9aaaaaaaa10" + + "aaaaaaaa11aaaaaaaa12aaaaaaaa13aaaaaaaa14aaaaaaaa15aaaaaaaa16aaaaaaaa17aaaaaaaa18aaaaaaaa19aaaaaaaa20" + + "aaaaaaaa21aaaaaaaa22aaaaaaaa23aaaaaaaa24aa:example.org" + ) + assertEquals( + expected = 254, + actual = longId.length, + ) + assertEquals( + expected = Mention.User(UserId(UserId.sigilCharacter + longId)), + actual = MatrixLinks.parse("https://matrix.to/#/@$longId") + ) + assertEquals( + expected = Mention.RoomAlias(RoomAliasId(RoomAliasId.sigilCharacter + longId)), + actual = MatrixLinks.parse("https://matrix.to/#/#$longId") + ) + assertEquals( + expected = Mention.Room(RoomId(RoomId.sigilCharacter + longId)), + actual = MatrixLinks.parse("https://matrix.to/#/!$longId") + ) + assertEquals( + expected = Mention.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("https://matrix.to/#/!$longId/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(RoomId(RoomId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId(EventId.sigilCharacter + longId)), + actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/$$longId") + ) + assertEquals( + expected = Mention.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + longId)), + actual = MatrixLinks.parse("https://matrix.to/#/!$longId/$$longId") + ) + assertEquals( + expected = Mention.Event(null, EventId(EventId.sigilCharacter + longId)), + actual = MatrixLinks.parse("https://matrix.to/#/$$longId") + ) + } + + @Test + fun `allows long matrix protocol links`() { + val longId = ( + "aaaaaaaaa1aaaaaaaaa2aaaaaaaaa3aaaaaaaaa4aaaaaaaaa5aaaaaaaaa6aaaaaaaaa7aaaaaaaaa8aaaaaaaaa9aaaaaaaa10" + + "aaaaaaaa11aaaaaaaa12aaaaaaaa13aaaaaaaa14aaaaaaaa15aaaaaaaa16aaaaaaaa17aaaaaaaa18aaaaaaaa19aaaaaaaa20" + + "aaaaaaaa21aaaaaaaa22aaaaaaaa23aaaaaaaa24aa:example.org" + ) + assertEquals( + expected = 254, + actual = longId.length, + ) + assertEquals( + expected = Mention.User(UserId(UserId.sigilCharacter + longId)), + actual = MatrixLinks.parse("matrix:u/$longId") + ) + assertEquals( + expected = Mention.RoomAlias(RoomAliasId(RoomAliasId.sigilCharacter + longId)), + actual = MatrixLinks.parse("matrix:r/$longId") + ) + assertEquals( + expected = Mention.Room(RoomId(RoomId.sigilCharacter + longId)), + actual = MatrixLinks.parse("matrix:roomid/$longId") + ) + assertEquals( + expected = Mention.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + actual = MatrixLinks.parse("matrix:roomid/$longId/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = Mention.Event(RoomId(RoomId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId(EventId.sigilCharacter + longId)), + actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$longId") + ) + assertEquals( + expected = Mention.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + longId)), + actual = MatrixLinks.parse("matrix:roomid/$longId/e/$longId") + ) + } + + @Test + fun `rejects too long matrixto links`() { + val tooLongId = ( + "aaaaaaaaa1aaaaaaaaa2aaaaaaaaa3aaaaaaaaa4aaaaaaaaa5aaaaaaaaa6aaaaaaaaa7aaaaaaaaa8aaaaaaaaa9aaaaaaaa10" + + "aaaaaaaa11aaaaaaaa12aaaaaaaa13aaaaaaaa14aaaaaaaa15aaaaaaaa16aaaaaaaa17aaaaaaaa18aaaaaaaa19aaaaaaaa20" + + "aaaaaaaa21aaaaaaaa22aaaaaaaa23aaaaaaaa24aaaaaaaa25aaaaaaaa26:example.org" + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("https://matrix.to/#/@$tooLongId") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("https://matrix.to/#/#$tooLongId") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("https://matrix.to/#/!$tooLongId") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("https://matrix.to/#/!$tooLongId/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$$tooLongId") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("https://matrix.to/#/!$tooLongId/$$tooLongId") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("https://matrix.to/#/$$tooLongId") + ) + } + + @Test + fun `rejects too long matrix protocol links`() { + val tooLongId = ( + "aaaaaaaaa1aaaaaaaaa2aaaaaaaaa3aaaaaaaaa4aaaaaaaaa5aaaaaaaaa6aaaaaaaaa7aaaaaaaaa8aaaaaaaaa9aaaaaaaa10" + + "aaaaaaaa11aaaaaaaa12aaaaaaaa13aaaaaaaa14aaaaaaaa15aaaaaaaa16aaaaaaaa17aaaaaaaa18aaaaaaaa19aaaaaaaa20" + + "aaaaaaaa21aaaaaaaa22aaaaaaaa23aaaaaaaa24aaaaaaaa25aaaaaaaa26:example.org" + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("matrix:u/$tooLongId") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("matrix:r/$tooLongId") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("matrix:roomid/$tooLongId") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("matrix:roomid/$tooLongId/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$tooLongId") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("matrix:roomid/$tooLongId/e/$tooLongId") + ) + assertEquals( + expected = null, + actual = MatrixLinks.parse("matrix:e/$tooLongId") + ) + } +} diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt new file mode 100644 index 000000000..ad7dba416 --- /dev/null +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt @@ -0,0 +1,673 @@ +package net.folivo.trixnity.core + +import io.kotest.matchers.shouldBe +import io.ktor.http.* +import net.folivo.trixnity.core.MatrixRegex.findMentions +import net.folivo.trixnity.core.model.EventId +import net.folivo.trixnity.core.model.Mention +import net.folivo.trixnity.core.model.RoomAliasId +import net.folivo.trixnity.core.model.RoomId +import net.folivo.trixnity.core.model.UserId +import net.folivo.trixnity.test.utils.TrixnityBaseTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import kotlin.test.fail + + +class MatrixRegexTest : TrixnityBaseTest() { + // User IDs + private fun userIdTest(id: String, localpart: String, domain: String, expected: Boolean) { + val text = "Hello $id :D" + val result = findMentions(text) + + result.keys.any { + text.substring(it) == id + } shouldBe expected + + if (expected) { + result.size shouldBe 1 + (result.entries.first { text.substring(it.key) == id }.value as Mention.User).userId shouldBe UserId( + localpart, + domain + ) + } else { + result.size shouldBe 0 + } + } + + @Test + fun shouldPassValidUserIdentifier() { + userIdTest("@a9._=-/+:example.com", "a9._=-/+", "example.com", expected = true) + } + + @Test + fun shouldPassValidUserIdentifierWithLongDomain() { + userIdTest( + "@demo.test:example.eu.timp.mock.abc.xyz", + "demo.test", + "example.eu.timp.mock.abc.xyz", + expected = true + ) + } + + @Test + fun shouldPassValidUserIdentifierWithSpecialCharacters() { + userIdTest("@user:sub.example.com:8000", "user", "sub.example.com:8000", expected = true) + } + + @Test + fun shouldPassValidUserIdentifierWithIPV4() { + userIdTest("@user:1.1.1.1", "user", "1.1.1.1", expected = true) + } + + @Test + fun shouldPassValidUserIdentifierWithIPV6() { + userIdTest( + "@user:[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + "user", + "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + expected = true + ) + } + + @Test + fun shouldFailUserOver255Bytes() { + userIdTest("@${"users".repeat(50)}:example.com", "users".repeat(50), "example.com", expected = false) + } + + @Test + fun shouldFailUserLocalpartContainingUppsercase() { + userIdTest("@User:example.com", "User", "example.com", expected = false) + } + + @Test + fun shouldFailUserLocalpartContainingIllegalSymbole() { + userIdTest("@user&:example.com", "user&", "example.com", expected = false) + } + + @Test + fun shouldFailInvalidUserIPV6WithIllegalCharacters() { + userIdTest("@user:[2001:8a2e:0370:733G]", "user", "[2001:8a2e:0370:733G]", expected = false) + } + + // Room Alias + private fun roomAliasTest(id: String, localpart: String, domain: String, expected: Boolean) { + val text = "omw to $id now" + val result = findMentions(text) + + result.keys.any { + text.substring(it) == id + } shouldBe expected + + if (expected) { + result.size shouldBe 1 + (result.entries.first { text.substring(it.key) == id }.value as Mention.RoomAlias).roomAliasId shouldBe RoomAliasId( + localpart, + domain + ) + } else { + result.size shouldBe 0 + } + } + + @Test + fun shouldPassRoomAlias() { + roomAliasTest("#a9._=-/+:example.com", "a9._=-/+", "example.com", expected = true) + } + + @Test + fun shouldPassRoomAliasWithPort() { + roomAliasTest("#room:sub.example.com:8000", "room", "sub.example.com:8000", expected = true) + } + + @Test + fun shouldPassRoomAliasWithIPV4() { + roomAliasTest("#room:1.1.1.1", "room", "1.1.1.1", expected = true) + } + + @Test + fun shouldPassRoomAliasWithIPV6() { + roomAliasTest( + "#room:[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + "room", + "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", + expected = true + ) + } + + @Test + fun shouldFailRoomAliasOver255Bytes() { + roomAliasTest("#${"alias".repeat(50)}:example.com", "alias".repeat(50), "example.com", expected = false) + } + + @Test + fun shouldFailRoomAliasWithIllegalSymboleInLocalpart() { + roomAliasTest("#roo&m:example.com", "room&", "example.com", expected = false) + } + + @Test + fun failFailRoomAliasIPV6WithIllegalCharacters() { + roomAliasTest("#room:[2001:8a2e:0370:733G]", "room", "[2001:8a2e:0370:733G]", expected = false) + } + + // URIs + private object UriTest { + fun user(uri: String, localpart: String, domain: String, expected: Boolean) { + val text = "Hello $uri :D" + val result = findMentions(text) + + result.keys.any { + text.substring(it) == uri + } shouldBe expected + + if (expected) { + result.size shouldBe 1 + (result.entries.first { text.substring(it.key) == uri }.value as Mention.User).userId shouldBe UserId( + localpart, + domain + ) + } else { + result.size shouldBe 0 + } + } + + fun roomId(uri: String, id: String, expected: Boolean) { + val text = "omw to $uri now" + val result = findMentions(text) + + result.keys.any { + text.substring(it) == uri + } shouldBe expected + + if (expected) { + result.size shouldBe 1 + (result.entries.first { text.substring(it.key) == uri }.value as Mention.Room).roomId shouldBe RoomId(id) + } + } + + fun roomAlias(uri: String, localpart: String, domain: String, expected: Boolean) { + val text = "omw to $uri now" + val result = findMentions(text) + + result.keys.any { + text.substring(it) == uri + } shouldBe expected + + if (expected) { + result.size shouldBe 1 + (result.entries.first { text.substring(it.key) == uri }.value as Mention.RoomAlias).roomAliasId shouldBe RoomAliasId( + localpart, + domain + ) + } else { + result.size shouldBe 0 + } + } + + fun event(uri: String, roomId: String, eventId: String, expected: Boolean) { + val text = "You can find it at $uri :)" + val result = findMentions(text) + + if (expected) { + val value = result.values.singleOrNull() + assertNotNull(value) + assertIs(value) + assertEquals(roomId, value.roomId!!.full) + assertEquals(eventId, value.eventId.full) + } else { + assertEquals( + expected = emptyList(), + actual = result.values.toList(), + ) + } + } + } + + // URIs: User ID + @Test + fun shouldPassUserURIWithActionQuery() { + UriTest.user("matrix:u/user:example.com?action=chat", "user", "example.com", expected = true) + } + + @Test + fun shouldPassUserURIWithViaQuery() { + UriTest.user("matrix:u/user:example.com?via=example.com", "user", "example.com", expected = true) + } + + @Test + fun shouldPassUserURIwithViaAndActionQuery() { + UriTest.user("matrix:u/user:example.com?via=example.com&action=chat", "user", "example.com", expected = true) + } + + @Test + fun shouldPassUserURIWithActionAndViaQuery() { + UriTest.user("matrix:u/user:example.com?action=chat&via=example.com", "user", "example.com", expected = true) + } + + @Test + fun shouldPassUserURI() { + UriTest.user("matrix:u/user:example.com", "user", "example.com", expected = true) + } + + // URIs: Room Alias + @Test + fun shouldPassRoomAliasURIWithActionQuery() { + UriTest.roomAlias("matrix:r/room:example.com?action=join", "room", "example.com", expected = true) + } + + @Test + fun shouldPassRoomAliasURIWithViaQuery() { + UriTest.roomAlias("matrix:r/room:example.com?via=example.com", "room", "example.com", expected = true) + } + + @Test + fun shouldPassRoomAliasURIWithViaAndActionQuery() { + UriTest.roomAlias( + "matrix:r/room:example.com?via=example.com&action=join", + "room", + "example.com", + expected = true + ) + } + + @Test + fun shouldPassRoomAliasURIWIthActionAndViaQuery() { + UriTest.roomAlias( + "matrix:r/room:example.com?action=chat&via=example.com", + "room", + "example.com", + expected = true + ) + } + + @Test + fun shouldPassRoomAliasURI() { + UriTest.roomAlias("matrix:r/room:example.com", "room", "example.com", expected = true) + } + + // URIs: Room ID + @Test + fun shouldPassRoomIdURIWithActionQuery() { + UriTest.roomId("matrix:roomid/room:example.com?action=join", "!room:example.com", expected = true) + } + + @Test + fun shouldPassRoomIdURIWithUnkownQuery() { + UriTest.roomId("matrix:roomid/room:example.com?actiooon=message", "!room:example.com", expected = true) + } + + @Test + fun shouldPassRoomIdURIWithIllegalQuery() { + UriTest.roomId("matrix:roomid/room:example.com?actioné=messager", "!room:example.com", expected = true) + } + + @Test + fun shouldPassRoomIdURIWithReservedQuery() { + UriTest.roomId("matrix:roomid/room:example.com?m.action=join", "!room:example.com", expected = true) + } + + @Test + fun shouldPassRoomIdURIWithViaQuery() { + UriTest.roomId("matrix:roomid/room:example.com?via=example.com", "!room:example.com", expected = true) + } + + @Test + fun shouldPassRoomIdURIWithViaAndActionQuery() { + UriTest.roomId( + "matrix:roomid/room:example.com?via=example.com&action=join", + "!room:example.com", + expected = true + ) + } + + @Test + fun shouldPassRoomIdURIWithActionAndViaQuery() { + UriTest.roomId( + "matrix:roomid/room:example.com?action=chat&via=example.com", + "!room:example.com", + expected = true + ) + } + + @Test + fun shouldPassRoomIdURI() { + UriTest.roomId("matrix:roomid/room:example.com", "!room:example.com", expected = true) + } + + // URIs: Event ID + @Test + fun shouldPassEventURIWithActionQuery() { + UriTest.event( + "matrix:roomid/room:example.com/e/event?action=join", + "!room:example.com", + "\$event", + expected = true + ) + } + + @Test + fun shouldPassEventURIWithViaQuery() { + UriTest.event( + "matrix:roomid/room:example.com/e/event?via=example.com", + "!room:example.com", + "\$event", + expected = true + ) + } + + @Test + fun shouldPassEventURIWithViaAndActionQuery() { + UriTest.event( + "matrix:roomid/room:example.com/e/event?via=example.com&action=join", + "!room:example.com", + "\$event", + expected = true + ) + } + + @Test + fun shouldPassEventURIWithActionAndViaQuery() { + UriTest.event( + "matrix:roomid/room:example.com/e/event?action=chat&via=example.com", + "!room:example.com", + "\$event", + expected = true + ) + } + + @Test + fun shouldPassEventURI() { + UriTest.event("matrix:roomid/room:example.com/e/event", "!room:example.com", "\$event", expected = true) + } + + // Permalinks (matrix.to) + object PermalinkTest { + fun user(permalink: String, localpart: String, domain: String, expected: Boolean) { + val text = "Hello $permalink :D" + val result = findMentions(text) + + result.keys.any { + text.substring(it) == permalink + } shouldBe expected + + if (expected) { + result.size shouldBe 1 + (result.entries.first { text.substring(it.key) == permalink }.value as Mention.User).userId shouldBe UserId( + localpart, + domain + ) + } else { + result.size shouldBe 0 + } + } + + fun roomId(permalink: String, roomId: String, expected: Boolean) { + val text = "omw to $permalink now" + val result = findMentions(text) + + result.keys.any { + text.substring(it) == permalink + } shouldBe expected + + if (expected) { + result.size shouldBe 1 + (result.entries.first { text.substring(it.key) == permalink }.value as Mention.Room).roomId shouldBe + RoomId(roomId) + } else { + result.size shouldBe 0 + } + } + + fun roomAlias(permalink: String, localpart: String, domain: String, expected: Boolean) { + val text = "omw to $permalink now" + val result = findMentions(text) + + result.keys.any { + text.substring(it) == permalink + } shouldBe expected + + if (expected) { + result.size shouldBe 1 + (result.entries.first { text.substring(it.key) == permalink }.value as Mention.RoomAlias).roomAliasId shouldBe RoomAliasId( + localpart, + domain + ) + } else { + result.size shouldBe 0 + } + } + + fun event(permalink: String, roomId: String, eventId: String, expected: Boolean) { + val text = "You can find it at $permalink :)" + val result = findMentions(text) + + result.keys.any { + text.substring(it) == permalink + } shouldBe expected + + if (expected) { + result.size shouldBe 1 + + val mention = result.entries.first { text.substring(it.key) == permalink }.value + if (mention !is Mention.Event) { + fail("Wrong Mention type") + } else { + mention.eventId shouldBe EventId(eventId) + mention.roomId shouldBe RoomId(roomId) + } + } else { + result.size shouldBe 0 + } + } + } + + // Permalink: User ID + + @Test + fun shouldPassUserPermalink() { + PermalinkTest.user("https://matrix.to/#/@user:example.com", "user", "example.com", expected = true) + } + + @Test + fun shouldPassEncodedUserPermalink() { + PermalinkTest.user("https://matrix.to/#/%40alice%3Aexample.org", "alice", "example.org", expected = true) + } + + // Permalink: Room Alias + + @Test + fun shouldPassRoomAliasPermalink() { + PermalinkTest.roomAlias("https://matrix.to/#/#room:example.com", "room", "example.com", expected = true) + } + + @Test + fun shouldPassEncodedRoomAliasPermalink() { + PermalinkTest.roomAlias( + "https://matrix.to/#/%23somewhere%3Aexample.org", + "somewhere", + "example.org", + expected = true + ) + } + + // Permalink: Room ID + + @Test + fun shouldPassRoomIdPermalink() { + PermalinkTest.roomId("https://matrix.to/#/!room:example.com", "!room:example.com", expected = true) + } + + @Test + fun shouldPassEncodedRoomIdPermalink() { + PermalinkTest.roomId( + "https://matrix.to/#/!room%3Aexample.com?via=elsewhere.ca", + "!room:example.com", + expected = true + ) + } + + // Permalink: Event ID + @Test + fun shouldPassEventIDPermalink() { + PermalinkTest.event( + "https://matrix.to/#/!room:example.com/\$event", + "!room:example.com", + "\$event", + expected = true + ) + } + + @Test + fun shouldPassEncodedEventIDPermalink() { + PermalinkTest.event( + "https://matrix.to/#/!room%3Aexample.com/%24event%3Aexample.org?via=elsewhere.ca", + "!room:example.com", + "\$event:example.org", + expected = true + ) + } + + // Parameters + fun parameterTest(uri: String, params: Parameters, expected: Boolean) { + val mentions = findMentions(uri) + + mentions.values.forEach { + (it.parameters == params) shouldBe expected + } + } + + @Test + fun shouldPassValidViaParameter() { + parameterTest( + "matrix:roomid/somewhere%3Aexample.org/%24event%3Aexample.org?via=elsewhere.ca", + parametersOf("via" to listOf("elsewhere.ca")), + expected = true + ) + } + + @Test + fun shouldPassActionParameter() { + parameterTest( + "matrix:roomid/room:example.com/e/event?via=example.com&action=join", + parametersOf("action" to listOf("join"), "via" to listOf("example.com")), + expected = true + ) + } + + @Test + fun shouldPassActionAndViaParameter() { + parameterTest( + "matrix:roomid/somewhere%3Aexample.org?action=chat&via=example.com", + parametersOf("action" to listOf("chat"), "via" to listOf("example.com")), + expected = true + ) + } + + @Test + fun shouldPassCustomParameter() { + parameterTest( + "matrix:r/somewhere:example.org?foo=bar", + parametersOf("foo" to listOf("bar")), + expected = true + ) + } + + @Test + fun shouldParseCustomParameterWithIllegalCharacter() { + parameterTest( + "matrix:u/mario:esempio.it?actionaté=mammamia", + parametersOf("actionaté" to listOf("mammamia")), + expected = true + ) + } + + @Test + fun shouldParseCustomParameterWithIllegalStart() { + parameterTest( + "matrix:u/user:homeserver.рф?m.vector=matrix", + parametersOf("m.vector" to listOf("matrix")), + expected = true + ) + } + + @Test + fun shouldParseCustomParametersWithLastOneBeingIllegal() { + parameterTest( + "matrix:u/user:example.com?foo=bar&actionaté=mammamia", + parametersOf("foo" to listOf("bar"), "actionaté" to listOf("mammamia")), + expected = true + ) + } + + @Test + fun `ignores overlaps`() { + val content = "lorem @user:example.org ipsum https://matrix.to/#/@user:example.org?action=chat dolor matrix:u/user:example.org sit" + // Links + assertEquals( + expected = "https://matrix.to/#/@user:example.org?action=chat", + actual = content.substring(30..78), + ) + assertEquals( + expected = "matrix:u/user:example.org", + actual = content.substring(86..110), + ) + assertEquals( + expected = mapOf( + 30..78 to Mention.User(UserId("@user:example.org"), parametersOf("action", "chat")), + 86..110 to Mention.User(UserId("@user:example.org")), + ), + actual = MatrixRegex.findLinkMentions(content) + ) + // Ids + assertEquals( + expected = "@user:example.org", + actual = content.substring(6..22), + ) + assertEquals( + expected = "@user:example.org", + actual = content.substring(50..66), + ) + assertEquals( + expected = mapOf( + 6..22 to Mention.User(UserId("@user:example.org")), + 50..66 to Mention.User(UserId("@user:example.org")), + ), + actual = MatrixRegex.findIdMentions(content) + ) + // Combined + assertEquals( + expected = mapOf( + 30..78 to Mention.User(UserId("@user:example.org"), parametersOf("action", "chat")), + 86..110 to Mention.User(UserId("@user:example.org")), + 6..22 to Mention.User(UserId("@user:example.org")), + ), + actual = MatrixRegex.findMentions(content) + ) + assertEquals( + expected = mapOf( + 9..44 to Mention.User(userId=UserId("@user:matrix.org")), + 92..171 to Mention.Room(roomId=RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), parameters=parametersOf("via" to listOf("matrix.org", "imbitbu.de"))), + 199..323 to Mention.Event(roomId=RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), eventId=EventId("\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A"), parameters=parametersOf("via" to listOf("matrix.org", "imbitbu.de"))), + ), + actual = MatrixRegex.findMentions( + "Some Username: This is a user mention
" + + "https://matrix.to/#/!WvOltebgJfkgHzhfpW:matrix.org?via=matrix.org&via=imbitbu.de This is a room mention
" + + "https://matrix.to/#/!WvOltebgJfkgHzhfpW:matrix.org/\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A?via=matrix.org&via=imbitbu.de This is an event mention" + ) + ) + } + + @Test + fun `finds regular links`() { + assertEquals( + expected = mapOf( + 19..65 to "https://en.wikipedia.org/wiki/Matrix_(protocol)", + ), + actual = MatrixRegex.findLinks( + "I saw that online (https://en.wikipedia.org/wiki/Matrix_(protocol)), neat eh?" + ) + ) + } +} diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/ReferencesTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/ReferencesTest.kt deleted file mode 100644 index ad4ebf6f6..000000000 --- a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/ReferencesTest.kt +++ /dev/null @@ -1,786 +0,0 @@ -package net.folivo.trixnity.core.util - -import io.kotest.matchers.maps.shouldBeEmpty -import io.kotest.matchers.maps.shouldContainExactly -import io.kotest.matchers.string.shouldHaveLength -import net.folivo.trixnity.core.model.EventId -import net.folivo.trixnity.core.model.RoomAliasId -import net.folivo.trixnity.core.model.RoomId -import net.folivo.trixnity.core.model.UserId -import net.folivo.trixnity.core.util.References.findIdReferences -import net.folivo.trixnity.core.util.References.findLinkReferences -import net.folivo.trixnity.core.util.References.findReferences -import net.folivo.trixnity.test.utils.TrixnityBaseTest -import kotlin.test.Test -import kotlin.test.assertEquals - -class ReferencesTest : TrixnityBaseTest() { - - @Test - fun shouldPassValidUserIdentifier() { - TestHelper.userId("@a9._=-/+:example.com", expected = true) - TestHelper.userId("@a9._=-/+:localhost", expected = true) - } - - @Test - fun shouldPassValidUserIdentifierWithLongDomain() { - TestHelper.userId( - "@demo.test:example.eu.timp.mock.abc.xyz", - expected = true - ) - } - - @Test - fun shouldPassValidUserIdentifierWithSpecialCharacters() { - TestHelper.userId("@user:sub.example.com:8000", expected = true) - } - - @Test - fun shouldPassValidUserIdentifierWithIPV4() { - TestHelper.userId("@user:1.1.1.1", expected = true) - } - - @Test - fun shouldPassValidUserIdentifierWithIPV6() { - TestHelper.userId( - "@user:[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", - expected = true - ) - } - - @Test - fun shouldFailUserOver255Bytes() { - TestHelper.userId("@${"users".repeat(50)}:example.com", expected = false) - } - - @Test - fun shouldFailUserLocalpartContainingUppsercase() { - TestHelper.userId("@User:example.com", expected = false) - } - - @Test - fun shouldFailUserLocalpartContainingIllegalSymbole() { - TestHelper.userId("@user&:example.com", expected = false) - } - - @Test - fun shouldFailInvalidUserIPV6WithIllegalCharacters() { - TestHelper.userId("@user:[2001:8a2e:0370:733G]", expected = false) - } - - @Test - fun shouldPassRoomAlias() { - TestHelper.roomAliasId("#a9._=-+:example.com", expected = true) - } - - @Test - fun shouldPassRoomAliasWithPort() { - TestHelper.roomAliasId("#room:sub.example.com:8000", expected = true) - } - - @Test - fun shouldPassRoomAliasWithIPV4() { - TestHelper.roomAliasId("#room:1.1.1.1", expected = true) - } - - @Test - fun shouldPassRoomAliasWithIPV6() { - TestHelper.roomAliasId( - "#room:[2001:0db8:85a3:0000:0000:8a2e:0370:7334]", - expected = true - ) - } - - @Test - fun shouldFailRoomAliasOver255Bytes() { - TestHelper.roomAliasId("#${"alias".repeat(50)}:example.com", expected = false) - } - - @Test - fun shouldFailRoomAliasWithIllegalSymboleInLocalpart() { - TestHelper.roomAliasId("#r/m:example.com", expected = false) - } - - @Test - fun failFailRoomAliasIPV6WithIllegalCharacters() { - TestHelper.roomAliasId("#room:[2001:8a2e:0370:733G]", expected = false) - } - - // URIs: User ID - @Test - fun shouldPassUserURIWithActionQuery() { - TestHelper.userId("matrix:u/user:example.com?action=chat", "@user:example.com", expected = true) - } - - @Test - fun shouldPassUserURIWithViaQuery() { - TestHelper.userId("matrix:u/user:example.com?via=example.com", "@user:example.com", expected = true) - } - - @Test - fun shouldPassUserURIwithViaAndActionQuery() { - TestHelper.userId("matrix:u/user:example.com?via=example.com&action=chat", "@user:example.com", expected = true) - } - - @Test - fun shouldPassUserURIWithActionAndViaQuery() { - TestHelper.userId("matrix:u/user:example.com?action=chat&via=example.com", "@user:example.com", expected = true) - } - - @Test - fun shouldPassUserURI() { - TestHelper.userId("matrix:u/user:example.com", "@user:example.com", expected = true) - } - - // URIs: Room Alias - @Test - fun shouldPassRoomAliasURIWithActionQuery() { - TestHelper.roomAliasId("matrix:r/room:example.com?action=join", "#room:example.com", expected = true) - } - - @Test - fun shouldPassRoomAliasURIWithViaQuery() { - TestHelper.roomAliasId("matrix:r/room:example.com?via=example.com", "#room:example.com", expected = true) - } - - @Test - fun shouldPassRoomAliasURIWithViaAndActionQuery() { - TestHelper.roomAliasId( - "matrix:r/room:example.com?via=example.com&action=join", - "#room:example.com", - expected = true - ) - } - - @Test - fun shouldPassRoomAliasURIWIthActionAndViaQuery() { - TestHelper.roomAliasId( - "matrix:r/room:example.com?action=chat&via=example.com", - "#room:example.com", - expected = true - ) - } - - @Test - fun shouldPassRoomAliasURI() { - TestHelper.roomAliasId("matrix:r/room:example.com", "#room:example.com", expected = true) - } - - // URIs: Room ID - @Test - fun shouldPassRoomIdURIWithActionQuery() { - TestHelper.roomId("matrix:roomid/room:example.com?action=join", "!room:example.com", expected = true) - } - - @Test - fun shouldPassRoomIdURIWithUnkownQuery() { - TestHelper.roomId("matrix:roomid/room:example.com?actiooon=message", "!room:example.com", expected = true) - } - - @Test - fun shouldPassRoomIdURIWithIllegalQuery() { - TestHelper.roomId("matrix:roomid/room:example.com?actioné=messager", "!room:example.com", expected = true) - } - - @Test - fun shouldPassRoomIdURIWithReservedQuery() { - TestHelper.roomId("matrix:roomid/room:example.com?m.action=join", "!room:example.com", expected = true) - } - - @Test - fun shouldPassRoomIdURIWithViaQuery() { - TestHelper.roomId("matrix:roomid/room:example.com?via=example.com", "!room:example.com", expected = true) - } - - @Test - fun shouldPassRoomIdURIWithViaAndActionQuery() { - TestHelper.roomId( - "matrix:roomid/room:example.com?via=example.com&action=join", - "!room:example.com", - expected = true - ) - } - - @Test - fun shouldPassRoomIdURIWithActionAndViaQuery() { - TestHelper.roomId( - "matrix:roomid/room:example.com?action=chat&via=example.com", - "!room:example.com", - expected = true - ) - } - - @Test - fun shouldPassRoomIdURI() { - TestHelper.roomId("matrix:roomid/room:example.com", "!room:example.com", expected = true) - } - - @Test - fun `parses matrix protocol roomid v12 links`() { - TestHelper.roomId( - "matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.roomId( - "matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - } - - // URIs: Event ID - @Test - fun shouldPassEventURIWithActionQuery() { - TestHelper.eventId( - "matrix:roomid/room:example.com/e/event?action=join", - "!room:example.com", - "\$event", - expected = true - ) - } - - @Test - fun shouldPassEventURIWithViaQuery() { - TestHelper.eventId( - "matrix:roomid/room:example.com/e/event?via=example.com", - "!room:example.com", - "\$event", - expected = true - ) - } - - @Test - fun shouldPassEventURIWithViaAndActionQuery() { - TestHelper.eventId( - "matrix:roomid/room:example.com/e/event?via=example.com&action=join", - "!room:example.com", - "\$event", - expected = true - ) - } - - @Test - fun shouldPassEventURIWithActionAndViaQuery() { - TestHelper.eventId( - "matrix:roomid/room:example.com/e/event?action=chat&via=example.com", - "!room:example.com", - "\$event", - expected = true - ) - } - - @Test - fun shouldPassEventURI() { - TestHelper.eventId("matrix:roomid/room:example.com/e/event", "!room:example.com", "\$event", expected = true) - } - - @Test - fun `parses matrix protocol event v12 links`() { - TestHelper.eventId( - "matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.eventId( - "matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - } - - // URIs: links - @Test - fun `finds regular links`() { - TestHelper.link("https://en.wikipedia.org/wiki/Matrix_(protocol)", true) - TestHelper.link("wikipedia.org", true) - } - - @Test - fun `fails on invalid links`() { - TestHelper.link("invalid-link", false) - TestHelper.link("matrix:group/group:example.com", false) - } - - // Permalink: User ID - - @Test - fun shouldPassUserPermalink() { - TestHelper.userId("https://matrix.to/#/@user:example.com", "@user:example.com", expected = true) - } - - @Test - fun shouldPassEncodedUserPermalink() { - TestHelper.userId("https://matrix.to/#/%40alice%3Aexample.org", "@alice:example.org", expected = true) - } - - // Permalink: Room Alias - - @Test - fun shouldPassRoomAliasPermalink() { - TestHelper.roomAliasId("https://matrix.to/#/#room:example.com", "#room:example.com", expected = true) - } - - @Test - fun shouldPassEncodedRoomAliasPermalink() { - TestHelper.roomAliasId( - "https://matrix.to/#/%23somewhere%3Aexample.org", - "#somewhere:example.org", - expected = true - ) - } - - // Permalink: Room ID - - @Test - fun shouldPassRoomIdPermalink() { - TestHelper.roomId("https://matrix.to/#/!room:example.com", "!room:example.com", expected = true) - } - - @Test - fun shouldPassEncodedRoomIdPermalink() { - TestHelper.roomId( - "https://matrix.to/#/!room%3Aexample.com?via=elsewhere.ca", - "!room:example.com", - expected = true - ) - } - - @Test - fun `parses matrixto roomid v12 links`() { - TestHelper.roomId( - "https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.roomId( - "https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.roomId( - "https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.roomId( - "https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - } - - // Permalink: Event ID - @Test - fun shouldPassEventIDPermalink() { - TestHelper.eventId( - "https://matrix.to/#/!room:example.com/\$event", - "!room:example.com", - "\$event", - expected = true - ) - } - - @Test - fun shouldPassEncodedEventIDPermalink() { - TestHelper.eventId( - "https://matrix.to/#/!room%3Aexample.com/%24event%3Aexample.org?via=elsewhere.ca", - "!room:example.com", - "\$event:example.org", - expected = true - ) - } - - @Test - fun `parses matrixto event v12 links`() { - TestHelper.eventId( - "https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.eventId( - "https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.eventId( - "https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.eventId( - "https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.eventId( - "https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - null, - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.eventId( - "https://matrix.to/#/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - null, - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.eventId( - "https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca", - null, - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - TestHelper.eventId( - "https://matrix.to/#%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca", - null, - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - expected = true - ) - } - - @Test - fun `ignores overlaps`() { - val content = - "lorem @user:example.org ipsum https://matrix.to/#/@user:example.org?action=chat dolor matrix:u/user:example.org sit" - // Links - assertEquals( - expected = "https://matrix.to/#/@user:example.org?action=chat", - actual = content.substring(30..78), - ) - assertEquals( - expected = "matrix:u/user:example.org", - actual = content.substring(86..110), - ) - assertEquals( - expected = mapOf( - 12..22 to Reference.Link("example.org"), - 30..78 to Reference.User( - UserId("@user:example.org"), - "https://matrix.to/#/@user:example.org?action=chat" - ), - 86..110 to Reference.User(UserId("@user:example.org"), "matrix:u/user:example.org"), - ), - actual = findLinkReferences(content) - ) - // Ids - assertEquals( - expected = "@user:example.org", - actual = content.substring(6..22), - ) - assertEquals( - expected = "@user:example.org", - actual = content.substring(50..66), - ) - assertEquals( - expected = mapOf( - 6..22 to Reference.User(UserId("@user:example.org")), - 50..66 to Reference.User(UserId("@user:example.org")), - ), - actual = findIdReferences(content) - ) - // Combined - assertEquals( - expected = mapOf( - 6..22 to Reference.User(UserId("@user:example.org")), - 30..78 to Reference.User( - UserId("@user:example.org"), - "https://matrix.to/#/@user:example.org?action=chat" - ), - 86..110 to Reference.User(UserId("@user:example.org"), "matrix:u/user:example.org"), - ), - actual = findReferences(content) - ) - assertEquals( - expected = mapOf( - 9..44 to Reference.User(UserId("@user:matrix.org"), "https://matrix.to/#/@user:matrix.org"), - 92..171 to Reference.Room( - roomId = RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), - uri = "https://matrix.to/#/!WvOltebgJfkgHzhfpW:matrix.org?via=matrix.org&via=imbitbu.de" - ), - 199..323 to Reference.Event( - roomId = RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), - eventId = EventId("\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A"), - uri = "https://matrix.to/#/!WvOltebgJfkgHzhfpW:matrix.org/\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A?via=matrix.org&via=imbitbu.de" - ), - ), - actual = findReferences( - "Some Username: This is a user mention
" + - "https://matrix.to/#/!WvOltebgJfkgHzhfpW:matrix.org?via=matrix.org&via=imbitbu.de This is a room mention
" + - "https://matrix.to/#/!WvOltebgJfkgHzhfpW:matrix.org/\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A?via=matrix.org&via=imbitbu.de This is an event mention" - ) - ) - } - - @Test - fun `allows long matrixto links`() { - val longId = ( - "aaaaaaaaa1aaaaaaaaa2aaaaaaaaa3aaaaaaaaa4aaaaaaaaa5aaaaaaaaa6aaaaaaaaa7aaaaaaaaa8aaaaaaaaa9aaaaaaaa10" + - "aaaaaaaa11aaaaaaaa12aaaaaaaa13aaaaaaaa14aaaaaaaa15aaaaaaaa16aaaaaaaa17aaaaaaaa18aaaaaaaa19aaaaaaaa20" + - "aaaaaaaa21aaaaaaaa22aaaaaaaa23aaaaaaaa24aa:example.org" - ) - longId shouldHaveLength 254 - - TestHelper.userId( - "https://matrix.to/#/@$longId", - "@$longId", - true - ) - TestHelper.roomAliasId( - "https://matrix.to/#/#$longId", - "#$longId", - true - ) - TestHelper.roomId( - "https://matrix.to/#/!$longId", - "!$longId", - true - ) - TestHelper.eventId( - "https://matrix.to/#/!$longId/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "!$longId", - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - true - ) - TestHelper.eventId( - "https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/$$longId", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "\$$longId", - true - ) - TestHelper.eventId( - "https://matrix.to/#/!$longId/$$longId", - "!$longId", - "\$$longId", - true - ) - TestHelper.eventId( - "https://matrix.to/#/$$longId", - null, - "\$$longId", - true - ) - } - - @Test - fun `rejects too long matrixto links`() { - val longId = ( - "aaaaaaaaa1aaaaaaaaa2aaaaaaaaa3aaaaaaaaa4aaaaaaaaa5aaaaaaaaa6aaaaaaaaa7aaaaaaaaa8aaaaaaaaa9aaaaaaaa10" + - "aaaaaaaa11aaaaaaaa12aaaaaaaa13aaaaaaaa14aaaaaaaa15aaaaaaaa16aaaaaaaa17aaaaaaaa18aaaaaaaa19aaaaaaaa20" + - "aaaaaaaa21aaaaaaaa22aaaaaaaa23aaaaaaaa24aaa:example.org" - ) - longId shouldHaveLength 255 - - TestHelper.userId( - "https://matrix.to/#/@$longId", - "@$longId", - false - ) - TestHelper.roomAliasId( - "https://matrix.to/#/#$longId", - "#$longId", - false - ) - TestHelper.roomId( - "https://matrix.to/#/!$longId", - "!$longId", - false - ) - TestHelper.eventId( - "https://matrix.to/#/!$longId/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "!$longId", - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - false - ) - TestHelper.roomId( // eventId ignored - "https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/$$longId", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - true - ) - TestHelper.eventId( - "https://matrix.to/#/!$longId/$$longId", - "!$longId", - "\$$longId", - false - ) - TestHelper.eventId( - "https://matrix.to/#/$$longId", - null, - "\$$longId", - false - ) - } - - @Test - fun `allow long matrix links`() { - val longId = ( - "aaaaaaaaa1aaaaaaaaa2aaaaaaaaa3aaaaaaaaa4aaaaaaaaa5aaaaaaaaa6aaaaaaaaa7aaaaaaaaa8aaaaaaaaa9aaaaaaaa10" + - "aaaaaaaa11aaaaaaaa12aaaaaaaa13aaaaaaaa14aaaaaaaa15aaaaaaaa16aaaaaaaa17aaaaaaaa18aaaaaaaa19aaaaaaaa20" + - "aaaaaaaa21aaaaaaaa22aaaaaaaa23aaaaaaaa24aa:example.org" - ) - longId shouldHaveLength 254 - - TestHelper.userId( - "matrix:u/$longId", - "@$longId", - true - ) - TestHelper.roomAliasId( - "matrix:r/$longId", - "#$longId", - true - ) - TestHelper.roomId( - "matrix:roomid/$longId", - "!$longId", - true - ) - TestHelper.eventId( - "matrix:roomid/$longId/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "!$longId", - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - true - ) - TestHelper.eventId( - "matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$longId", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "\$$longId", - true - ) - TestHelper.eventId( - "matrix:roomid/$longId/e/$longId", - "!$longId", - "\$$longId", - true - ) - } - - @Test - fun `rejects too long matrix links`() { - val longId = ( - "aaaaaaaaa1aaaaaaaaa2aaaaaaaaa3aaaaaaaaa4aaaaaaaaa5aaaaaaaaa6aaaaaaaaa7aaaaaaaaa8aaaaaaaaa9aaaaaaaa10" + - "aaaaaaaa11aaaaaaaa12aaaaaaaa13aaaaaaaa14aaaaaaaa15aaaaaaaa16aaaaaaaa17aaaaaaaa18aaaaaaaa19aaaaaaaa20" + - "aaaaaaaa21aaaaaaaa22aaaaaaaa23aaaaaaaa24aaa:example.org" - ) - longId shouldHaveLength 255 - - TestHelper.userId( - "matrix:u/$longId", - "@$longId", - false - ) - TestHelper.roomAliasId( - "matrix:r/$longId", - "#$longId", - false - ) - TestHelper.roomId( - "matrix:roomid/$longId", - "!$longId", - false - ) - TestHelper.eventId( - "matrix:roomid/$longId/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - "!$longId", - "\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - false - ) - TestHelper.roomId( // ignores event id - "matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$longId", - "!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE", - true - ) - TestHelper.eventId( - "matrix:roomid/$longId/e/$longId", - "!$longId", - "\$$longId", - false - ) - } -} - -private object TestHelper { - fun userId(reference: String, expected: Boolean) = userId(reference, reference, expected, true) - fun userId(reference: String, userId: String, expected: Boolean, isId: Boolean = false) { - val result = findReferences("prefix $reference suffix") - - if (expected) { - result shouldContainExactly mapOf( - 7..(7 + reference.lastIndex) to Reference.User( - UserId(userId), - if (!isId) reference else null - ) - ) - } else { - if (isId) result.filterValues { it !is Reference.Link }.shouldBeEmpty() - else result.shouldBeEmpty() - } - } - - fun roomId(reference: String, roomId: String, expected: Boolean) { - val result = findReferences("prefix $reference suffix") - - if (expected) { - result shouldContainExactly mapOf( - 7..(7 + reference.lastIndex) to Reference.Room( - RoomId(roomId), - reference - ) - ) - } else { - result.shouldBeEmpty() - } - } - - fun roomAliasId(reference: String, expected: Boolean) = roomAliasId(reference, reference, expected, true) - - fun roomAliasId(reference: String, id: String, expected: Boolean, isId: Boolean = false) { - val result = findReferences("prefix $reference suffix") - - if (expected) { - result shouldContainExactly mapOf( - 7..(7 + reference.lastIndex) to Reference.RoomAlias( - RoomAliasId(id), - if (!isId) reference else null - ) - ) - } else { - if (isId) result.filterValues { it !is Reference.Link }.shouldBeEmpty() - else result.shouldBeEmpty() - } - } - - fun eventId(reference: String, roomId: String?, eventId: String, expected: Boolean) { - val result = findReferences("prefix $reference suffix") - - if (expected) { - result shouldContainExactly mapOf( - 7..(7 + reference.lastIndex) to Reference.Event( - roomId?.let(::RoomId), - EventId(eventId), - reference - ) - ) - } else { - result.shouldBeEmpty() - } - } - - fun link(reference: String, expected: Boolean) { - val result = findReferences("prefix $reference suffix") - - if (expected) { - result shouldContainExactly mapOf( - 7..(7 + reference.lastIndex) to Reference.Link(reference) - ) - } else { - result.shouldBeEmpty() - } - } -} \ No newline at end of file -- GitLab From a6439deba1a8c55075bd45fe95f62432991f2109 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 10:19:59 +0200 Subject: [PATCH 02/12] Extract autolink and validation regexes --- .../net/folivo/trixnity/core/MatrixRegex.kt | 17 +---- .../trixnity/core/util/MatrixIdRegex.kt | 74 +++++++++++++++++++ .../folivo/trixnity/core/MatrixRegexTest.kt | 5 -- 3 files changed, 76 insertions(+), 20 deletions(-) create mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt index 51c355407..6713610af 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt @@ -4,16 +4,13 @@ import io.github.oshai.kotlinlogging.KotlinLogging import net.folivo.trixnity.core.model.Mention import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.UserId +import net.folivo.trixnity.core.util.MatrixIdRegex import net.folivo.trixnity.core.util.MatrixLinks import net.folivo.trixnity.core.util.Patterns private val log = KotlinLogging.logger {} object MatrixRegex { - // language=Regexp - private const val ID_PATTERN = """[@#][0-9a-z\-.=_/+]+:(?:[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|\[[0-9a-fA-F:.]{2,45}\]|[0-9a-zA-Z\-.]{1,255})(?::[0-9]{1,5})?""" - private val idRegex = ID_PATTERN.toRegex() - fun findMentions(message: String): Map { val links = findLinkMentions(message) val users = findIdMentions(message) @@ -27,7 +24,7 @@ object MatrixRegex { } fun findIdMentions(content: String, from: Int = 0, to: Int = content.length): Map { - return idRegex + return MatrixIdRegex.autolinkId .findAll(content, startIndex = from) .filter { it.range.last < to } .filter { it.range.last - it.range.first <= 255 } @@ -61,16 +58,6 @@ object MatrixRegex { }.toMap() } - fun isValidUserId(id: String): Boolean = - id.length <= 255 - && id.startsWith(UserId.sigilCharacter) - && id.matches(idRegex) - - fun isValidRoomAliasId(id: String): Boolean = - id.length <= 255 - && id.startsWith(RoomAliasId.sigilCharacter) - && id.matches(idRegex) - private fun parseMatrixId(id: String): Mention? { return when { id.length > 255 -> { diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt new file mode 100644 index 000000000..0b3785e05 --- /dev/null +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt @@ -0,0 +1,74 @@ +package net.folivo.trixnity.core.util + +internal object MatrixIdRegex { + /** + * @see [https://spec.matrix.org/unstable/appendices/#server-name] + */ + // language=Regexp + private const val DOMAIN_PATTERN = """(?:[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|\[[0-9a-fA-F:.]{2,45}\]|[0-9a-zA-Z\-.]{1,255})(?::[0-9]{1,5})?""" + + /** + * @see [https://spec.matrix.org/v1.14/appendices/#opaque-identifiers] + */ + // language=Regexp + private const val OPAQUE_ID_PATTERN = "[0-9A-Za-z-._~]+" + + /** + * clients and servers MUST accept user IDs with localparts consisting of any legal non-surrogate Unicode + * code points except for : and NUL (U+0000), including other control characters and the empty string. + * + * @see [https://spec.matrix.org/unstable/appendices/#user-identifiers] + */ + // language=Regexp + private const val USER_ID_PATTERN = "@[^:\uD800-\uDFFF]+:$DOMAIN_PATTERN" + val userId = USER_ID_PATTERN.toRegex() + + // language=Regexp + private const val REASONABLE_USER_ID_PATTERN = "@[0-9a-z-=_/+.]+:$DOMAIN_PATTERN" + val reasonableUserId = REASONABLE_USER_ID_PATTERN.toRegex() + + /** + * The localpart of a room alias may contain any valid non-surrogate Unicode codepoints except : and NUL. + * + * @see [https://spec.matrix.org/unstable/appendices/#room-aliases] + */ + // language=Regexp + private const val ROOM_ALIAS_PATTERN = "#[^:\uD800-\uDFFF]+:$DOMAIN_PATTERN" + val roomAlias = ROOM_ALIAS_PATTERN.toRegex() + + /** + * The localpart of a room ID (opaque_id above) may contain any valid non-surrogate Unicode code points, + * including control characters, except : and NUL (U+0000) + * + * @see [https://spec.matrix.org/unstable/appendices/#room-ids] + */ + // language=Regexp + private const val ROOM_ID_PATTERN = "!(?:$OPAQUE_ID_PATTERN|[^:\uD800-\uDFFF]+:$DOMAIN_PATTERN)" + val roomId = ROOM_ID_PATTERN.toRegex() + + // language=Regexp + private const val REASONABLE_ROOM_ID_PATTERN = "!$OPAQUE_ID_PATTERN(?::$DOMAIN_PATTERN)?" + val reasonableRoomId = ROOM_ID_PATTERN.toRegex() + + /** + * However, the precise format depends upon the room version specification: early room versions included + * a domain component, whereas more recent versions omit the domain and use a base64-encoded hash instead. + * + * @see [https://spec.matrix.org/unstable/appendices/#event-ids] + */ + // language=Regexp + private const val EVENT_ID_PATTERN = "\\$(?:$OPAQUE_ID_PATTERN|[^:\uD800-\uDFFF]+:$DOMAIN_PATTERN)" + val eventId = EVENT_ID_PATTERN.toRegex() + + // language=Regexp + private const val REASONABLE_EVENT_ID_PATTERN = "\\$$OPAQUE_ID_PATTERN(?::$DOMAIN_PATTERN)?" + val reasonableEventId = REASONABLE_EVENT_ID_PATTERN.toRegex() + + /** + * This matches user ids and room aliases on best-effort basis, + * it is NOT intended to match all valid ids. + */ + // language=Regexp + private const val AUTOLINK_ID_PATTERN = "[@#](?:\\p{L}|\\p{N}|[\\-.=_/+])+:$DOMAIN_PATTERN" + val autolinkId = AUTOLINK_ID_PATTERN.toRegex() +} \ No newline at end of file diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt index ad7dba416..0118df174 100644 --- a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt @@ -77,11 +77,6 @@ class MatrixRegexTest : TrixnityBaseTest() { userIdTest("@${"users".repeat(50)}:example.com", "users".repeat(50), "example.com", expected = false) } - @Test - fun shouldFailUserLocalpartContainingUppsercase() { - userIdTest("@User:example.com", "User", "example.com", expected = false) - } - @Test fun shouldFailUserLocalpartContainingIllegalSymbole() { userIdTest("@user&:example.com", "user&", "example.com", expected = false) -- GitLab From e4c673b86af9c52af3d841edd1b3ddbfb9db70a9 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 10:29:50 +0200 Subject: [PATCH 03/12] Reintroduce id validation --- .../net/folivo/trixnity/core/model/EventId.kt | 7 +++++++ .../folivo/trixnity/core/model/RoomAliasId.kt | 5 +++++ .../net/folivo/trixnity/core/model/RoomId.kt | 16 ++++++---------- .../net/folivo/trixnity/core/model/UserId.kt | 7 +++++++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/EventId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/EventId.kt index 3519c0562..234dc32bd 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/EventId.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/EventId.kt @@ -7,13 +7,20 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import net.folivo.trixnity.core.util.MatrixIdRegex @Serializable(with = EventIdSerializer::class) data class EventId(val full: String) { companion object { const val sigilCharacter = '$' + + fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.eventId) + fun isReasonable(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.reasonableEventId) } + val isValid by lazy { isValid(full) } + val isReasonable by lazy { isReasonable(full) } + override fun toString() = full } diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomAliasId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomAliasId.kt index b610887bf..b88a12125 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomAliasId.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomAliasId.kt @@ -7,6 +7,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import net.folivo.trixnity.core.util.MatrixIdRegex @Serializable(with = RoomAliasIdSerializer::class) data class RoomAliasId(val full: String) { @@ -15,6 +16,8 @@ data class RoomAliasId(val full: String) { companion object { const val sigilCharacter = '#' + + fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.roomAlias) } val localpart: String @@ -22,6 +25,8 @@ data class RoomAliasId(val full: String) { val domain: String get() = full.trimStart(sigilCharacter).substringAfter(':') + val isValid by lazy { isValid(full) } + override fun toString() = full } diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomId.kt index 9cbf470ac..16ffdf6d4 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomId.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/RoomId.kt @@ -7,24 +7,20 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import net.folivo.trixnity.core.util.MatrixIdRegex @Serializable(with = RoomIdSerializer::class) data class RoomId(val full: String) { - @Deprecated("RoomId should be considered as opaque String") - constructor(localpart: String, domain: String) : this("${sigilCharacter}$localpart:$domain") - companion object { const val sigilCharacter = '!' - } - @Deprecated("RoomId should be considered as opaque String") - val localpart: String - get() = full.trimStart(sigilCharacter).substringBefore(':') + fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.roomId) + fun isReasonable(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.reasonableRoomId) + } - @Deprecated("RoomId should be considered as opaque String") - val domain: String - get() = full.trimStart(sigilCharacter).substringAfter(':') + val isValid by lazy { isValid(full) } + val isReasonable by lazy { isReasonable(full) } override fun toString() = full } diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/UserId.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/UserId.kt index 5924e7ae2..dfe934a6f 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/UserId.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/UserId.kt @@ -7,6 +7,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import net.folivo.trixnity.core.util.MatrixIdRegex @Serializable(with = UserIdSerializer::class) data class UserId(val full: String) { @@ -15,6 +16,9 @@ data class UserId(val full: String) { companion object { const val sigilCharacter = '@' + + fun isValid(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.userId) + fun isReasonable(id: String): Boolean = id.length <= 255 && id.matches(MatrixIdRegex.reasonableUserId) } val localpart: String @@ -22,6 +26,9 @@ data class UserId(val full: String) { val domain: String get() = full.trimStart(sigilCharacter).substringAfter(':') + val isValid by lazy { isValid(full) } + val isReasonable by lazy { isReasonable(full) } + override fun toString() = full } -- GitLab From 87b7f80acb165f6cb85f201a06ade28b9857198c Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 10:37:16 +0200 Subject: [PATCH 04/12] Refactor "Mention" to "Reference" --- .../net/folivo/trixnity/core/MatrixRegex.kt | 14 +-- .../folivo/trixnity/core/util/MatrixLinks.kt | 25 ++-- .../{model/Mention.kt => util/Reference.kt} | 19 +-- .../folivo/trixnity/core/MatrixLinkTest.kt | 116 +++++++++--------- .../folivo/trixnity/core/MatrixRegexTest.kt | 48 ++++---- 5 files changed, 111 insertions(+), 111 deletions(-) rename trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/{model/Mention.kt => util/Reference.kt} (71%) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt index 6713610af..cd4a827fe 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt @@ -1,17 +1,17 @@ package net.folivo.trixnity.core import io.github.oshai.kotlinlogging.KotlinLogging -import net.folivo.trixnity.core.model.Mention import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.UserId import net.folivo.trixnity.core.util.MatrixIdRegex import net.folivo.trixnity.core.util.MatrixLinks import net.folivo.trixnity.core.util.Patterns +import net.folivo.trixnity.core.util.Reference private val log = KotlinLogging.logger {} object MatrixRegex { - fun findMentions(message: String): Map { + fun findMentions(message: String): Map { val links = findLinkMentions(message) val users = findIdMentions(message) val linksRange = links.keys.sortedBy { it.first } @@ -23,7 +23,7 @@ object MatrixRegex { return links.plus(uniqueUsers).toMap() } - fun findIdMentions(content: String, from: Int = 0, to: Int = content.length): Map { + fun findIdMentions(content: String, from: Int = 0, to: Int = content.length): Map { return MatrixIdRegex.autolinkId .findAll(content, startIndex = from) .filter { it.range.last < to } @@ -32,7 +32,7 @@ object MatrixRegex { .toMap() } - fun findLinkMentions(content: String, from: Int = 0, to: Int = content.length): Map { + fun findLinkMentions(content: String, from: Int = 0, to: Int = content.length): Map { return Patterns.AUTOLINK_MATRIX_URI .findAll(content, startIndex = from) .filter { it.range.last < to } @@ -58,14 +58,14 @@ object MatrixRegex { }.toMap() } - private fun parseMatrixId(id: String): Mention? { + private fun parseMatrixId(id: String): Reference? { return when { id.length > 255 -> { log.trace { "malformed matrix id: id too long: ${id.length} (max length: 255)" } null } - id.startsWith(UserId.sigilCharacter) -> Mention.User(UserId(id)) - id.startsWith(RoomAliasId.sigilCharacter) -> Mention.RoomAlias(RoomAliasId(id)) + id.startsWith(UserId.sigilCharacter) -> Reference.User(UserId(id)) + id.startsWith(RoomAliasId.sigilCharacter) -> Reference.RoomAlias(RoomAliasId(id)) else -> null } } diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt index fc9113ea7..b9afb9d32 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt @@ -3,7 +3,6 @@ package net.folivo.trixnity.core.util import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.* import net.folivo.trixnity.core.model.EventId -import net.folivo.trixnity.core.model.Mention import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId @@ -13,7 +12,7 @@ private val log = KotlinLogging.logger {} object MatrixLinks { private val matrixProtocol = URLProtocol("matrix", 0) - fun parse(href: String): Mention? { + fun parse(href: String): Reference? { val url = Url(href) if (url.protocol == matrixProtocol) { return parseMatrixProtocol(url.segments, url.parameters) @@ -34,7 +33,7 @@ object MatrixLinks { return null } - private fun parseMatrixTo(path: List, parameters: Parameters): Mention? { + private fun parseMatrixTo(path: List, parameters: Parameters): Reference? { val parts = path.map { id -> when { id.length > 255 -> { @@ -54,11 +53,11 @@ object MatrixLinks { val first = parts.getOrNull(0) val second = parts.getOrNull(1) return when { - first is UserId -> Mention.User(first, parameters) - first is RoomAliasId -> Mention.RoomAlias(first, parameters) - first is EventId -> Mention.Event(null, first, parameters) - first is RoomId && second is EventId -> Mention.Event(first, second, parameters) - first is RoomId -> Mention.Room(first, parameters) + first is UserId -> Reference.User(first, parameters) + first is RoomAliasId -> Reference.RoomAlias(first, parameters) + first is EventId -> Reference.Event(null, first, parameters) + first is RoomId && second is EventId -> Reference.Event(first, second, parameters) + first is RoomId -> Reference.Room(first, parameters) else -> { log.trace { "malformed matrix link: unknown format" } null @@ -66,7 +65,7 @@ object MatrixLinks { } } - private fun parseMatrixProtocol(path: List, parameters: Parameters): Mention? { + private fun parseMatrixProtocol(path: List, parameters: Parameters): Reference? { val parts = path.windowed(2, 2).map { (type, id) -> when { id.length > 255 -> { @@ -86,10 +85,10 @@ object MatrixLinks { val first = parts.getOrNull(0) val second = parts.getOrNull(1) return when { - first is UserId -> Mention.User(first, parameters) - first is RoomAliasId -> Mention.RoomAlias(first, parameters) - first is RoomId && second is EventId -> Mention.Event(first, second, parameters) - first is RoomId -> Mention.Room(first, parameters) + first is UserId -> Reference.User(first, parameters) + first is RoomAliasId -> Reference.RoomAlias(first, parameters) + first is RoomId && second is EventId -> Reference.Event(first, second, parameters) + first is RoomId -> Reference.Room(first, parameters) else -> { log.trace { "malformed matrix link: unknown format" } null diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/Mention.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt similarity index 71% rename from trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/Mention.kt rename to trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt index 1ffecedfd..b7f03057f 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/model/Mention.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt @@ -1,11 +1,16 @@ -package net.folivo.trixnity.core.model +package net.folivo.trixnity.core.util -import io.ktor.http.* +import io.ktor.http.Parameters +import io.ktor.http.parametersOf +import net.folivo.trixnity.core.model.EventId +import net.folivo.trixnity.core.model.RoomAliasId +import net.folivo.trixnity.core.model.RoomId +import net.folivo.trixnity.core.model.UserId /** * Represents a mention. A mention can refer to various entities and potentially include actions associated with them. */ -sealed interface Mention { +sealed interface Reference { /** * If exists, the parameters provided in the URI */ @@ -18,7 +23,7 @@ sealed interface Mention { data class User( val userId: UserId, override val parameters: Parameters? = parametersOf() - ) : Mention + ) : Reference /** * Represents a mention of a room. @@ -26,7 +31,7 @@ sealed interface Mention { data class Room( val roomId: RoomId, override val parameters: Parameters? = parametersOf() - ) : Mention + ) : Reference /** * Represents a mention of a room alias @@ -34,7 +39,7 @@ sealed interface Mention { data class RoomAlias( val roomAliasId: RoomAliasId, override val parameters: Parameters? = parametersOf() - ) : Mention + ) : Reference /** * Represents a mention of a generic event. @@ -43,5 +48,5 @@ sealed interface Mention { val roomId: RoomId? = null, val eventId: EventId, override val parameters: Parameters? = parametersOf() - ) : Mention + ) : Reference } \ No newline at end of file diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt index fda231702..8c8c35120 100644 --- a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt @@ -2,11 +2,11 @@ package net.folivo.trixnity.core import io.ktor.http.* import net.folivo.trixnity.core.model.EventId -import net.folivo.trixnity.core.model.Mention import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId import net.folivo.trixnity.core.util.MatrixLinks +import net.folivo.trixnity.core.util.Reference import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull @@ -24,19 +24,19 @@ class MatrixLinkTest { @Test fun `parses matrixto user links`() { assertEquals( - expected = Mention.User(UserId("@user:example.com")), + expected = Reference.User(UserId("@user:example.com")), actual = MatrixLinks.parse("https://matrix.to/#/@user:example.com") ) assertEquals( - expected = Mention.User(UserId("@user:example.com")), + expected = Reference.User(UserId("@user:example.com")), actual = MatrixLinks.parse("https://matrix.to/#%2F%40user%3Aexample.com") ) assertEquals( - expected = Mention.User(UserId("@user:example.com"), parametersOf("action", "chat")), + expected = Reference.User(UserId("@user:example.com"), parametersOf("action", "chat")), actual = MatrixLinks.parse("https://matrix.to/#/@user:example.com?action=chat") ) assertEquals( - expected = Mention.User(UserId("@user:example.com"), parametersOf("action", "chat")), + expected = Reference.User(UserId("@user:example.com"), parametersOf("action", "chat")), actual = MatrixLinks.parse("https://matrix.to/#%2F%40user%3Aexample.com%3Faction=chat") ) } @@ -44,11 +44,11 @@ class MatrixLinkTest { @Test fun `parses matrix protocol user links`() { assertEquals( - expected = Mention.User(UserId("@user:example.com")), + expected = Reference.User(UserId("@user:example.com")), actual = MatrixLinks.parse("matrix:u/user:example.com") ) assertEquals( - expected = Mention.User(UserId("@user:example.com"), parametersOf("action", "chat")), + expected = Reference.User(UserId("@user:example.com"), parametersOf("action", "chat")), actual = MatrixLinks.parse("matrix:u/user:example.com?action=chat") ) } @@ -56,22 +56,22 @@ class MatrixLinkTest { @Test fun `parses matrixto room alias links`() { assertEquals( - expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org")), + expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org")), actual = MatrixLinks.parse("https://matrix.to/#/#somewhere:example.org") ) assertEquals( - expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org")), + expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org")), actual = MatrixLinks.parse("https://matrix.to/#%2F#somewhere%3Aexample.org") ) assertEquals( - expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( + expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), actual = MatrixLinks.parse("https://matrix.to/#/#somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( - expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( + expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), @@ -82,11 +82,11 @@ class MatrixLinkTest { @Test fun `parses matrix protocol room alias links`() { assertEquals( - expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org")), + expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org")), actual = MatrixLinks.parse("matrix:r/somewhere:example.org") ) assertEquals( - expected = Mention.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( + expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), @@ -97,22 +97,22 @@ class MatrixLinkTest { @Test fun `parses matrixto roomid links`() { assertEquals( - expected = Mention.Room(RoomId("!somewhere:example.org")), + expected = Reference.Room(RoomId("!somewhere:example.org")), actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org") ) assertEquals( - expected = Mention.Room(RoomId("!somewhere:example.org")), + expected = Reference.Room(RoomId("!somewhere:example.org")), actual = MatrixLinks.parse("https://matrix.to/#%2F!somewhere%3Aexample.org") ) assertEquals( - expected = Mention.Room(RoomId("!somewhere:example.org"),parametersOf( + expected = Reference.Room(RoomId("!somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( - expected = Mention.Room(RoomId("!somewhere:example.org"),parametersOf( + expected = Reference.Room(RoomId("!somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), @@ -123,11 +123,11 @@ class MatrixLinkTest { @Test fun `parses matrix protocol roomid links`() { assertEquals( - expected = Mention.Room(RoomId("!somewhere:example.org")), + expected = Reference.Room(RoomId("!somewhere:example.org")), actual = MatrixLinks.parse("matrix:roomid/somewhere:example.org") ) assertEquals( - expected = Mention.Room(RoomId("!somewhere:example.org"),parametersOf( + expected = Reference.Room(RoomId("!somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), @@ -138,22 +138,22 @@ class MatrixLinkTest { @Test fun `parses matrixto roomid v12 links`() { assertEquals( - expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( - expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), @@ -164,11 +164,11 @@ class MatrixLinkTest { @Test fun `parses matrix protocol roomid v12 links`() { assertEquals( - expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), @@ -179,44 +179,44 @@ class MatrixLinkTest { @Test fun `parses matrixto event links`() { assertEquals( - expected = Mention.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#/!somewhere%3Aexample.org/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(RoomId("!somewhere:example.org"),EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( + expected = Reference.Event(RoomId("!somewhere:example.org"),EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( - expected = Mention.Event(RoomId("!somewhere:example.org"),EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( + expected = Reference.Event(RoomId("!somewhere:example.org"),EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), actual = MatrixLinks.parse("https://matrix.to/#%2F!somewhere%3Aexample.org%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") ) assertEquals( - expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( - expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), @@ -227,11 +227,11 @@ class MatrixLinkTest { @Test fun `parses matrix protocol event links`() { assertEquals( - expected = Mention.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("matrix:roomid/somewhere:example.org/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + expected = Reference.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), @@ -242,44 +242,44 @@ class MatrixLinkTest { @Test fun `parses matrixto event v12 links`() { assertEquals( - expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( + expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( - expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( + expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), actual = MatrixLinks.parse("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") ) assertEquals( - expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( - expected = Mention.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), @@ -290,11 +290,11 @@ class MatrixLinkTest { @Test fun `parses matrix protocol event v12 links`() { assertEquals( - expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( + expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), @@ -314,31 +314,31 @@ class MatrixLinkTest { actual = longId.length, ) assertEquals( - expected = Mention.User(UserId(UserId.sigilCharacter + longId)), + expected = Reference.User(UserId(UserId.sigilCharacter + longId)), actual = MatrixLinks.parse("https://matrix.to/#/@$longId") ) assertEquals( - expected = Mention.RoomAlias(RoomAliasId(RoomAliasId.sigilCharacter + longId)), + expected = Reference.RoomAlias(RoomAliasId(RoomAliasId.sigilCharacter + longId)), actual = MatrixLinks.parse("https://matrix.to/#/#$longId") ) assertEquals( - expected = Mention.Room(RoomId(RoomId.sigilCharacter + longId)), + expected = Reference.Room(RoomId(RoomId.sigilCharacter + longId)), actual = MatrixLinks.parse("https://matrix.to/#/!$longId") ) assertEquals( - expected = Mention.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("https://matrix.to/#/!$longId/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(RoomId(RoomId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId(EventId.sigilCharacter + longId)), + expected = Reference.Event(RoomId(RoomId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId(EventId.sigilCharacter + longId)), actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/$$longId") ) assertEquals( - expected = Mention.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + longId)), + expected = Reference.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + longId)), actual = MatrixLinks.parse("https://matrix.to/#/!$longId/$$longId") ) assertEquals( - expected = Mention.Event(null, EventId(EventId.sigilCharacter + longId)), + expected = Reference.Event(null, EventId(EventId.sigilCharacter + longId)), actual = MatrixLinks.parse("https://matrix.to/#/$$longId") ) } @@ -355,27 +355,27 @@ class MatrixLinkTest { actual = longId.length, ) assertEquals( - expected = Mention.User(UserId(UserId.sigilCharacter + longId)), + expected = Reference.User(UserId(UserId.sigilCharacter + longId)), actual = MatrixLinks.parse("matrix:u/$longId") ) assertEquals( - expected = Mention.RoomAlias(RoomAliasId(RoomAliasId.sigilCharacter + longId)), + expected = Reference.RoomAlias(RoomAliasId(RoomAliasId.sigilCharacter + longId)), actual = MatrixLinks.parse("matrix:r/$longId") ) assertEquals( - expected = Mention.Room(RoomId(RoomId.sigilCharacter + longId)), + expected = Reference.Room(RoomId(RoomId.sigilCharacter + longId)), actual = MatrixLinks.parse("matrix:roomid/$longId") ) assertEquals( - expected = Mention.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), + expected = Reference.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), actual = MatrixLinks.parse("matrix:roomid/$longId/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( - expected = Mention.Event(RoomId(RoomId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId(EventId.sigilCharacter + longId)), + expected = Reference.Event(RoomId(RoomId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId(EventId.sigilCharacter + longId)), actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$longId") ) assertEquals( - expected = Mention.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + longId)), + expected = Reference.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + longId)), actual = MatrixLinks.parse("matrix:roomid/$longId/e/$longId") ) } diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt index 0118df174..8a21fda0f 100644 --- a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt @@ -4,16 +4,12 @@ import io.kotest.matchers.shouldBe import io.ktor.http.* import net.folivo.trixnity.core.MatrixRegex.findMentions import net.folivo.trixnity.core.model.EventId -import net.folivo.trixnity.core.model.Mention import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId +import net.folivo.trixnity.core.util.Reference import net.folivo.trixnity.test.utils.TrixnityBaseTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertNotNull -import kotlin.test.fail +import kotlin.test.* class MatrixRegexTest : TrixnityBaseTest() { @@ -28,7 +24,7 @@ class MatrixRegexTest : TrixnityBaseTest() { if (expected) { result.size shouldBe 1 - (result.entries.first { text.substring(it.key) == id }.value as Mention.User).userId shouldBe UserId( + (result.entries.first { text.substring(it.key) == id }.value as Reference.User).userId shouldBe UserId( localpart, domain ) @@ -98,7 +94,7 @@ class MatrixRegexTest : TrixnityBaseTest() { if (expected) { result.size shouldBe 1 - (result.entries.first { text.substring(it.key) == id }.value as Mention.RoomAlias).roomAliasId shouldBe RoomAliasId( + (result.entries.first { text.substring(it.key) == id }.value as Reference.RoomAlias).roomAliasId shouldBe RoomAliasId( localpart, domain ) @@ -159,7 +155,7 @@ class MatrixRegexTest : TrixnityBaseTest() { if (expected) { result.size shouldBe 1 - (result.entries.first { text.substring(it.key) == uri }.value as Mention.User).userId shouldBe UserId( + (result.entries.first { text.substring(it.key) == uri }.value as Reference.User).userId shouldBe UserId( localpart, domain ) @@ -178,7 +174,7 @@ class MatrixRegexTest : TrixnityBaseTest() { if (expected) { result.size shouldBe 1 - (result.entries.first { text.substring(it.key) == uri }.value as Mention.Room).roomId shouldBe RoomId(id) + (result.entries.first { text.substring(it.key) == uri }.value as Reference.Room).roomId shouldBe RoomId(id) } } @@ -192,7 +188,7 @@ class MatrixRegexTest : TrixnityBaseTest() { if (expected) { result.size shouldBe 1 - (result.entries.first { text.substring(it.key) == uri }.value as Mention.RoomAlias).roomAliasId shouldBe RoomAliasId( + (result.entries.first { text.substring(it.key) == uri }.value as Reference.RoomAlias).roomAliasId shouldBe RoomAliasId( localpart, domain ) @@ -208,7 +204,7 @@ class MatrixRegexTest : TrixnityBaseTest() { if (expected) { val value = result.values.singleOrNull() assertNotNull(value) - assertIs(value) + assertIs(value) assertEquals(roomId, value.roomId!!.full) assertEquals(eventId, value.eventId.full) } else { @@ -389,7 +385,7 @@ class MatrixRegexTest : TrixnityBaseTest() { if (expected) { result.size shouldBe 1 - (result.entries.first { text.substring(it.key) == permalink }.value as Mention.User).userId shouldBe UserId( + (result.entries.first { text.substring(it.key) == permalink }.value as Reference.User).userId shouldBe UserId( localpart, domain ) @@ -408,7 +404,7 @@ class MatrixRegexTest : TrixnityBaseTest() { if (expected) { result.size shouldBe 1 - (result.entries.first { text.substring(it.key) == permalink }.value as Mention.Room).roomId shouldBe + (result.entries.first { text.substring(it.key) == permalink }.value as Reference.Room).roomId shouldBe RoomId(roomId) } else { result.size shouldBe 0 @@ -425,7 +421,7 @@ class MatrixRegexTest : TrixnityBaseTest() { if (expected) { result.size shouldBe 1 - (result.entries.first { text.substring(it.key) == permalink }.value as Mention.RoomAlias).roomAliasId shouldBe RoomAliasId( + (result.entries.first { text.substring(it.key) == permalink }.value as Reference.RoomAlias).roomAliasId shouldBe RoomAliasId( localpart, domain ) @@ -446,7 +442,7 @@ class MatrixRegexTest : TrixnityBaseTest() { result.size shouldBe 1 val mention = result.entries.first { text.substring(it.key) == permalink }.value - if (mention !is Mention.Event) { + if (mention !is Reference.Event) { fail("Wrong Mention type") } else { mention.eventId shouldBe EventId(eventId) @@ -610,8 +606,8 @@ class MatrixRegexTest : TrixnityBaseTest() { ) assertEquals( expected = mapOf( - 30..78 to Mention.User(UserId("@user:example.org"), parametersOf("action", "chat")), - 86..110 to Mention.User(UserId("@user:example.org")), + 30..78 to Reference.User(UserId("@user:example.org"), parametersOf("action", "chat")), + 86..110 to Reference.User(UserId("@user:example.org")), ), actual = MatrixRegex.findLinkMentions(content) ) @@ -626,25 +622,25 @@ class MatrixRegexTest : TrixnityBaseTest() { ) assertEquals( expected = mapOf( - 6..22 to Mention.User(UserId("@user:example.org")), - 50..66 to Mention.User(UserId("@user:example.org")), + 6..22 to Reference.User(UserId("@user:example.org")), + 50..66 to Reference.User(UserId("@user:example.org")), ), actual = MatrixRegex.findIdMentions(content) ) // Combined assertEquals( expected = mapOf( - 30..78 to Mention.User(UserId("@user:example.org"), parametersOf("action", "chat")), - 86..110 to Mention.User(UserId("@user:example.org")), - 6..22 to Mention.User(UserId("@user:example.org")), + 30..78 to Reference.User(UserId("@user:example.org"), parametersOf("action", "chat")), + 86..110 to Reference.User(UserId("@user:example.org")), + 6..22 to Reference.User(UserId("@user:example.org")), ), actual = MatrixRegex.findMentions(content) ) assertEquals( expected = mapOf( - 9..44 to Mention.User(userId=UserId("@user:matrix.org")), - 92..171 to Mention.Room(roomId=RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), parameters=parametersOf("via" to listOf("matrix.org", "imbitbu.de"))), - 199..323 to Mention.Event(roomId=RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), eventId=EventId("\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A"), parameters=parametersOf("via" to listOf("matrix.org", "imbitbu.de"))), + 9..44 to Reference.User(userId=UserId("@user:matrix.org")), + 92..171 to Reference.Room(roomId=RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), parameters=parametersOf("via" to listOf("matrix.org", "imbitbu.de"))), + 199..323 to Reference.Event(roomId=RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), eventId=EventId("\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A"), parameters=parametersOf("via" to listOf("matrix.org", "imbitbu.de"))), ), actual = MatrixRegex.findMentions( "Some Username: This is a user mention
" + -- GitLab From 70e4c448eb0daba598b1eac14dada3a0a713712f Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 10:40:03 +0200 Subject: [PATCH 05/12] Add link references --- .../net/folivo/trixnity/core/util/Reference.kt | 18 ++++++++---------- .../folivo/trixnity/core/MatrixRegexTest.kt | 9 ++++++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt index b7f03057f..aad2a30f8 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt @@ -11,18 +11,12 @@ import net.folivo.trixnity.core.model.UserId * Represents a mention. A mention can refer to various entities and potentially include actions associated with them. */ sealed interface Reference { - /** - * If exists, the parameters provided in the URI - */ - val parameters: Parameters? - - /** * Represents a mention of a user. */ data class User( val userId: UserId, - override val parameters: Parameters? = parametersOf() + val parameters: Parameters = parametersOf() ) : Reference /** @@ -30,7 +24,7 @@ sealed interface Reference { */ data class Room( val roomId: RoomId, - override val parameters: Parameters? = parametersOf() + val parameters: Parameters = parametersOf() ) : Reference /** @@ -38,7 +32,7 @@ sealed interface Reference { */ data class RoomAlias( val roomAliasId: RoomAliasId, - override val parameters: Parameters? = parametersOf() + val parameters: Parameters = parametersOf() ) : Reference /** @@ -47,6 +41,10 @@ sealed interface Reference { data class Event( val roomId: RoomId? = null, val eventId: EventId, - override val parameters: Parameters? = parametersOf() + val parameters: Parameters = parametersOf() + ) : Reference + + data class Link( + val url: String ) : Reference } \ No newline at end of file diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt index 8a21fda0f..488fe8c6a 100644 --- a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt @@ -525,7 +525,14 @@ class MatrixRegexTest : TrixnityBaseTest() { val mentions = findMentions(uri) mentions.values.forEach { - (it.parameters == params) shouldBe expected + val parameters = when (it) { + is Reference.Event -> it.parameters + is Reference.Link -> null + is Reference.Room -> it.parameters + is Reference.RoomAlias -> it.parameters + is Reference.User -> it.parameters + } + (parameters == params) shouldBe expected } } -- GitLab From ce30983f66a1dc8bb20097b0c77812a66263d6f4 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 11:06:24 +0200 Subject: [PATCH 06/12] Move matrixregex to references --- .../core/{MatrixRegex.kt => util/References.kt} | 8 ++------ .../net/folivo/trixnity/core/MatrixRegexTest.kt | 15 +++++++++------ 2 files changed, 11 insertions(+), 12 deletions(-) rename trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/{MatrixRegex.kt => util/References.kt} (93%) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt similarity index 93% rename from trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt rename to trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt index cd4a827fe..c45c4943b 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/MatrixRegex.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt @@ -1,16 +1,12 @@ -package net.folivo.trixnity.core +package net.folivo.trixnity.core.util import io.github.oshai.kotlinlogging.KotlinLogging import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.UserId -import net.folivo.trixnity.core.util.MatrixIdRegex -import net.folivo.trixnity.core.util.MatrixLinks -import net.folivo.trixnity.core.util.Patterns -import net.folivo.trixnity.core.util.Reference private val log = KotlinLogging.logger {} -object MatrixRegex { +object References { fun findMentions(message: String): Map { val links = findLinkMentions(message) val users = findIdMentions(message) diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt index 488fe8c6a..bbe34df3a 100644 --- a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt @@ -2,7 +2,10 @@ package net.folivo.trixnity.core import io.kotest.matchers.shouldBe import io.ktor.http.* -import net.folivo.trixnity.core.MatrixRegex.findMentions +import net.folivo.trixnity.core.util.References.findMentions +import net.folivo.trixnity.core.util.References.findIdMentions +import net.folivo.trixnity.core.util.References.findLinkMentions +import net.folivo.trixnity.core.util.References.findLinks import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId @@ -616,7 +619,7 @@ class MatrixRegexTest : TrixnityBaseTest() { 30..78 to Reference.User(UserId("@user:example.org"), parametersOf("action", "chat")), 86..110 to Reference.User(UserId("@user:example.org")), ), - actual = MatrixRegex.findLinkMentions(content) + actual = findLinkMentions(content) ) // Ids assertEquals( @@ -632,7 +635,7 @@ class MatrixRegexTest : TrixnityBaseTest() { 6..22 to Reference.User(UserId("@user:example.org")), 50..66 to Reference.User(UserId("@user:example.org")), ), - actual = MatrixRegex.findIdMentions(content) + actual = findIdMentions(content) ) // Combined assertEquals( @@ -641,7 +644,7 @@ class MatrixRegexTest : TrixnityBaseTest() { 86..110 to Reference.User(UserId("@user:example.org")), 6..22 to Reference.User(UserId("@user:example.org")), ), - actual = MatrixRegex.findMentions(content) + actual = findMentions(content) ) assertEquals( expected = mapOf( @@ -649,7 +652,7 @@ class MatrixRegexTest : TrixnityBaseTest() { 92..171 to Reference.Room(roomId=RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), parameters=parametersOf("via" to listOf("matrix.org", "imbitbu.de"))), 199..323 to Reference.Event(roomId=RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), eventId=EventId("\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A"), parameters=parametersOf("via" to listOf("matrix.org", "imbitbu.de"))), ), - actual = MatrixRegex.findMentions( + actual = findMentions( "Some Username: This is a user mention
" + "https://matrix.to/#/!WvOltebgJfkgHzhfpW:matrix.org?via=matrix.org&via=imbitbu.de This is a room mention
" + "https://matrix.to/#/!WvOltebgJfkgHzhfpW:matrix.org/\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A?via=matrix.org&via=imbitbu.de This is an event mention" @@ -663,7 +666,7 @@ class MatrixRegexTest : TrixnityBaseTest() { expected = mapOf( 19..65 to "https://en.wikipedia.org/wiki/Matrix_(protocol)", ), - actual = MatrixRegex.findLinks( + actual = findLinks( "I saw that online (https://en.wikipedia.org/wiki/Matrix_(protocol)), neat eh?" ) ) -- GitLab From dcf602a9e5725634dea703b9f188af856fc0ec14 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 11:07:51 +0200 Subject: [PATCH 07/12] Unify parsing of links and mentions --- .../folivo/trixnity/core/util/MatrixLinks.kt | 98 ----------- .../folivo/trixnity/core/util/Reference.kt | 158 ++++++++++++++++++ .../folivo/trixnity/core/util/References.kt | 95 ----------- .../FindReferencesTest.kt} | 56 +++---- .../ParseLinkTest.kt} | 158 +++++++++--------- 5 files changed, 255 insertions(+), 310 deletions(-) delete mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt delete mode 100644 trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt rename trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/{MatrixRegexTest.kt => util/FindReferencesTest.kt} (92%) rename trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/{MatrixLinkTest.kt => util/ParseLinkTest.kt} (63%) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt deleted file mode 100644 index b9afb9d32..000000000 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixLinks.kt +++ /dev/null @@ -1,98 +0,0 @@ -package net.folivo.trixnity.core.util - -import io.github.oshai.kotlinlogging.KotlinLogging -import io.ktor.http.* -import net.folivo.trixnity.core.model.EventId -import net.folivo.trixnity.core.model.RoomAliasId -import net.folivo.trixnity.core.model.RoomId -import net.folivo.trixnity.core.model.UserId - -private val log = KotlinLogging.logger {} - -object MatrixLinks { - private val matrixProtocol = URLProtocol("matrix", 0) - - fun parse(href: String): Reference? { - val url = Url(href) - if (url.protocol == matrixProtocol) { - return parseMatrixProtocol(url.segments, url.parameters) - } - // matrix.to URLs look like this: - // https://matrix.to/#/!roomId?via=example.org - // protocol=https host=matrix.to segments=[] fragment=/!roomId?via=example.org - if (url.protocol == URLProtocol.HTTPS && url.host == "matrix.to" && url.segments.isEmpty()) { - // matrix.to uses AJAX hash routing, where the entire path is passed within the hash fragment to prevent - // the server from seeing the roomId. - // This means we have to parse this hash back into path segments and query parameters - val path = url.fragment.substringBefore('?').removePrefix("/") - val query = url.fragment.substringAfter('?', missingDelimiterValue = "") - val segments = path.removePrefix("/").split('/') - val parameters = parseQueryString(query, decode = false) - return parseMatrixTo(segments, parameters) - } - return null - } - - private fun parseMatrixTo(path: List, parameters: Parameters): Reference? { - val parts = path.map { id -> - when { - id.length > 255 -> { - log.trace { "malformed matrix link: id too long: ${id.length} (max length: 255)" } - return null - } - id.startsWith(RoomAliasId.sigilCharacter) -> RoomAliasId(id) - id.startsWith(RoomId.sigilCharacter) -> RoomId(id) - id.startsWith(UserId.sigilCharacter) -> UserId(id) - id.startsWith(EventId.sigilCharacter) -> EventId(id) - else -> { - log.trace { "malformed matrix link: invalid id type: ${id.firstOrNull()} (known types: #, !, @, $)" } - null - } - } - } - val first = parts.getOrNull(0) - val second = parts.getOrNull(1) - return when { - first is UserId -> Reference.User(first, parameters) - first is RoomAliasId -> Reference.RoomAlias(first, parameters) - first is EventId -> Reference.Event(null, first, parameters) - first is RoomId && second is EventId -> Reference.Event(first, second, parameters) - first is RoomId -> Reference.Room(first, parameters) - else -> { - log.trace { "malformed matrix link: unknown format" } - null - } - } - } - - private fun parseMatrixProtocol(path: List, parameters: Parameters): Reference? { - val parts = path.windowed(2, 2).map { (type, id) -> - when { - id.length > 255 -> { - log.trace { "malformed matrix link: id too long: ${id.length} (max length: 255)" } - return null - } - type == "roomid" -> RoomId("!$id") - type == "r" -> RoomAliasId("#$id") - type == "u" -> UserId("@$id") - type == "e" -> EventId("$$id") - else -> { - log.trace { "malformed matrix link: invalid id type: $type (known types: roomid, r, u, e)" } - null - } - } - } - val first = parts.getOrNull(0) - val second = parts.getOrNull(1) - return when { - first is UserId -> Reference.User(first, parameters) - first is RoomAliasId -> Reference.RoomAlias(first, parameters) - first is RoomId && second is EventId -> Reference.Event(first, second, parameters) - first is RoomId -> Reference.Room(first, parameters) - else -> { - log.trace { "malformed matrix link: unknown format" } - null - } - } - } -} \ No newline at end of file diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt index aad2a30f8..7ca336457 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt @@ -1,12 +1,18 @@ package net.folivo.trixnity.core.util +import io.github.oshai.kotlinlogging.KotlinLogging import io.ktor.http.Parameters +import io.ktor.http.URLProtocol +import io.ktor.http.Url import io.ktor.http.parametersOf +import io.ktor.http.parseQueryString import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId +private val log = KotlinLogging.logger {} + /** * Represents a mention. A mention can refer to various entities and potentially include actions associated with them. */ @@ -47,4 +53,156 @@ sealed interface Reference { data class Link( val url: String ) : Reference + + companion object { + fun findReferences(message: String): Map { + val candidates = findLinkReferences(message).plus(findIdReferences(message)) + return buildMap { + for ((candidateKey, candidateValue) in candidates) { + val overlapKey = keys.find { it.intersect(candidateKey).isNotEmpty() } + if (overlapKey == null) { + put(candidateKey, candidateValue) + } else if (overlapKey.last - overlapKey.first < candidateKey.last - candidateKey.first) { + remove(overlapKey) + put(candidateKey, candidateValue) + } + } + } + } + + private fun findIdReferences(content: String, from: Int = 0, to: Int = content.length): Map { + return MatrixIdRegex.autolinkId + .findAll(content, startIndex = from) + .filter { it.range.last < to } + .filter { it.range.last - it.range.first <= 255 } + .mapNotNull { Pair(it.range, parseMatrixId(it.value) ?: return@mapNotNull null) } + .toMap() + } + + private fun findLinkReferences(content: String, from: Int = 0, to: Int = content.length): Map { + return Patterns.AUTOLINK_MATRIX_URI + .findAll(content, startIndex = from) + .filter { it.range.last < to } + .associate { + val trimmedContent = it.value.trimLink() + Pair( + it.range.first.until(it.range.first + trimmedContent.length), + parseLink(trimmedContent) ?: Link(trimmedContent) + ) + } + } + + private fun parseMatrixId(id: String): Reference? { + return when { + id.length > 255 -> { + log.trace { "malformed matrix id: id too long: ${id.length} (max length: 255)" } + null + } + id.startsWith(UserId.sigilCharacter) -> User(UserId(id)) + id.startsWith(RoomAliasId.sigilCharacter) -> RoomAlias(RoomAliasId(id)) + else -> null + } + } + + fun parseLink(href: String): Reference? { + val url = Url(href) + if (url.protocol == matrixProtocol) { + return parseMatrixProtocolLink(url.segments, url.parameters) + } + // matrix.to URLs look like this: + // https://matrix.to/#/!roomId?via=example.org + // protocol=https host=matrix.to segments=[] fragment=/!roomId?via=example.org + if (url.protocol == URLProtocol.HTTPS && url.host == "matrix.to" && url.segments.isEmpty()) { + // matrix.to uses AJAX hash routing, where the entire path is passed within the hash fragment to prevent + // the server from seeing the roomId. + // This means we have to parse this hash back into path segments and query parameters + val path = url.fragment.substringBefore('?').removePrefix("/") + val query = url.fragment.substringAfter('?', missingDelimiterValue = "") + val segments = path.removePrefix("/").split('/') + val parameters = parseQueryString(query, decode = false) + return parseMatrixToLink(segments, parameters) + } + return null + } + + private fun parseMatrixToLink(path: List, parameters: Parameters): Reference? { + val parts = path.map { id -> + when { + id.length > 255 -> { + log.trace { "malformed matrix link: id too long: ${id.length} (max length: 255)" } + return null + } + id.startsWith(RoomAliasId.sigilCharacter) -> RoomAliasId(id) + id.startsWith(RoomId.sigilCharacter) -> RoomId(id) + id.startsWith(UserId.sigilCharacter) -> UserId(id) + id.startsWith(EventId.sigilCharacter) -> EventId(id) + else -> { + log.trace { "malformed matrix link: invalid id type: ${id.firstOrNull()} (known types: #, !, @, $)" } + null + } + } + } + val first = parts.getOrNull(0) + val second = parts.getOrNull(1) + return when { + first is UserId -> User(first, parameters) + first is RoomAliasId -> RoomAlias(first, parameters) + first is EventId -> Event(null, first, parameters) + first is RoomId && second is EventId -> Event(first, second, parameters) + first is RoomId -> Room(first, parameters) + else -> { + log.trace { "malformed matrix link: unknown format" } + null + } + } + } + + private fun parseMatrixProtocolLink(path: List, parameters: Parameters): Reference? { + val parts = path.windowed(2, 2).map { (type, id) -> + when { + id.length > 255 -> { + log.trace { "malformed matrix link: id too long: ${id.length} (max length: 255)" } + return null + } + type == "roomid" -> RoomId("!$id") + type == "r" -> RoomAliasId("#$id") + type == "u" -> UserId("@$id") + type == "e" -> EventId("$$id") + else -> { + log.trace { "malformed matrix link: invalid id type: $type (known types: roomid, r, u, e)" } + null + } + } + } + val first = parts.getOrNull(0) + val second = parts.getOrNull(1) + return when { + first is UserId -> User(first, parameters) + first is RoomAliasId -> RoomAlias(first, parameters) + first is RoomId && second is EventId -> Event(first, second, parameters) + first is RoomId -> Room(first, parameters) + else -> { + log.trace { "malformed matrix link: unknown format" } + null + } + } + } + + private fun String.trimParens(): String = + if (endsWith(')')) { + val trimmed = trimEnd(')') + val openingParens = trimmed.count { it == '(' } + val closingParens = trimmed.count { it == ')' } + val endingParens = length - trimmed.length + val openParens = openingParens - closingParens + + val desiredParens = minOf(endingParens, openParens) + take(trimmed.length + desiredParens) + } else this + + private fun String.trimLink(): String = + trimEnd(',', '.', '!', '?', ':').trimParens() + + private val matrixProtocol = URLProtocol("matrix", 0) + } } \ No newline at end of file diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt deleted file mode 100644 index c45c4943b..000000000 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/References.kt +++ /dev/null @@ -1,95 +0,0 @@ -package net.folivo.trixnity.core.util - -import io.github.oshai.kotlinlogging.KotlinLogging -import net.folivo.trixnity.core.model.RoomAliasId -import net.folivo.trixnity.core.model.UserId - -private val log = KotlinLogging.logger {} - -object References { - fun findMentions(message: String): Map { - val links = findLinkMentions(message) - val users = findIdMentions(message) - val linksRange = links.keys.sortedBy { it.first } - val uniqueUsers = users.filter { (user, _) -> - // We don't want id matches that overlap with link matches, - // as matrix.to urls will match both as link and as id - !linksRange.overlaps(user) - } - return links.plus(uniqueUsers).toMap() - } - - fun findIdMentions(content: String, from: Int = 0, to: Int = content.length): Map { - return MatrixIdRegex.autolinkId - .findAll(content, startIndex = from) - .filter { it.range.last < to } - .filter { it.range.last - it.range.first <= 255 } - .mapNotNull { Pair(it.range, parseMatrixId(it.value) ?: return@mapNotNull null) } - .toMap() - } - - fun findLinkMentions(content: String, from: Int = 0, to: Int = content.length): Map { - return Patterns.AUTOLINK_MATRIX_URI - .findAll(content, startIndex = from) - .filter { it.range.last < to } - .mapNotNull { - val trimmedContent = it.value.trimLink() - Pair( - it.range.first.until(it.range.first + trimmedContent.length), - MatrixLinks.parse(trimmedContent) ?: return@mapNotNull null - ) - }.toMap() - } - - fun findLinks(content: String, from: Int = 0, to: Int = content.length): Map { - return Patterns.AUTOLINK_MATRIX_URI - .findAll(content, startIndex = from) - .filter { it.range.last < to } - .map { - val trimmedContent = it.value.trimLink() - Pair( - it.range.first.until(it.range.first + trimmedContent.length), - trimmedContent, - ) - }.toMap() - } - - private fun parseMatrixId(id: String): Reference? { - return when { - id.length > 255 -> { - log.trace { "malformed matrix id: id too long: ${id.length} (max length: 255)" } - null - } - id.startsWith(UserId.sigilCharacter) -> Reference.User(UserId(id)) - id.startsWith(RoomAliasId.sigilCharacter) -> Reference.RoomAlias(RoomAliasId(id)) - else -> null - } - } - - private fun List.overlaps(user: IntRange): Boolean { - val index = binarySearch { link -> - when { - user.last < link.first -> 1 - user.first > link.last -> -1 - user.first >= link.first && user.last <= link.last -> 0 - else -> -1 - } - } - return index >= 0 - } - - private fun String.trimParens(): String = - if (endsWith(')')) { - val trimmed = trimEnd(')') - val openingParens = trimmed.count { it == '(' } - val closingParens = trimmed.count { it == ')' } - val endingParens = length - trimmed.length - val openParens = openingParens - closingParens - - val desiredParens = minOf(endingParens, openParens) - take(trimmed.length + desiredParens) - } else this - - private fun String.trimLink(): String = - trimEnd(',', '.', '!', '?', ':').trimParens() -} \ No newline at end of file diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/FindReferencesTest.kt similarity index 92% rename from trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt rename to trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/FindReferencesTest.kt index bbe34df3a..c8836c1a3 100644 --- a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixRegexTest.kt +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/FindReferencesTest.kt @@ -1,25 +1,21 @@ -package net.folivo.trixnity.core +package net.folivo.trixnity.core.util import io.kotest.matchers.shouldBe import io.ktor.http.* -import net.folivo.trixnity.core.util.References.findMentions -import net.folivo.trixnity.core.util.References.findIdMentions -import net.folivo.trixnity.core.util.References.findLinkMentions -import net.folivo.trixnity.core.util.References.findLinks +import net.folivo.trixnity.core.util.Reference.Companion.findReferences import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId -import net.folivo.trixnity.core.util.Reference import net.folivo.trixnity.test.utils.TrixnityBaseTest import kotlin.test.* -class MatrixRegexTest : TrixnityBaseTest() { +class FindReferencesTest : TrixnityBaseTest() { // User IDs private fun userIdTest(id: String, localpart: String, domain: String, expected: Boolean) { val text = "Hello $id :D" - val result = findMentions(text) + val result = findReferences(text).filter { it.value !is Reference.Link } result.keys.any { text.substring(it) == id @@ -89,7 +85,7 @@ class MatrixRegexTest : TrixnityBaseTest() { // Room Alias private fun roomAliasTest(id: String, localpart: String, domain: String, expected: Boolean) { val text = "omw to $id now" - val result = findMentions(text) + val result = findReferences(text).filter { it.value !is Reference.Link } result.keys.any { text.substring(it) == id @@ -150,7 +146,7 @@ class MatrixRegexTest : TrixnityBaseTest() { private object UriTest { fun user(uri: String, localpart: String, domain: String, expected: Boolean) { val text = "Hello $uri :D" - val result = findMentions(text) + val result = findReferences(text) result.keys.any { text.substring(it) == uri @@ -169,7 +165,7 @@ class MatrixRegexTest : TrixnityBaseTest() { fun roomId(uri: String, id: String, expected: Boolean) { val text = "omw to $uri now" - val result = findMentions(text) + val result = findReferences(text) result.keys.any { text.substring(it) == uri @@ -183,7 +179,7 @@ class MatrixRegexTest : TrixnityBaseTest() { fun roomAlias(uri: String, localpart: String, domain: String, expected: Boolean) { val text = "omw to $uri now" - val result = findMentions(text) + val result = findReferences(text) result.keys.any { text.substring(it) == uri @@ -202,7 +198,7 @@ class MatrixRegexTest : TrixnityBaseTest() { fun event(uri: String, roomId: String, eventId: String, expected: Boolean) { val text = "You can find it at $uri :)" - val result = findMentions(text) + val result = findReferences(text) if (expected) { val value = result.values.singleOrNull() @@ -380,7 +376,7 @@ class MatrixRegexTest : TrixnityBaseTest() { object PermalinkTest { fun user(permalink: String, localpart: String, domain: String, expected: Boolean) { val text = "Hello $permalink :D" - val result = findMentions(text) + val result = findReferences(text) result.keys.any { text.substring(it) == permalink @@ -399,7 +395,7 @@ class MatrixRegexTest : TrixnityBaseTest() { fun roomId(permalink: String, roomId: String, expected: Boolean) { val text = "omw to $permalink now" - val result = findMentions(text) + val result = findReferences(text) result.keys.any { text.substring(it) == permalink @@ -416,7 +412,7 @@ class MatrixRegexTest : TrixnityBaseTest() { fun roomAlias(permalink: String, localpart: String, domain: String, expected: Boolean) { val text = "omw to $permalink now" - val result = findMentions(text) + val result = findReferences(text) result.keys.any { text.substring(it) == permalink @@ -435,7 +431,7 @@ class MatrixRegexTest : TrixnityBaseTest() { fun event(permalink: String, roomId: String, eventId: String, expected: Boolean) { val text = "You can find it at $permalink :)" - val result = findMentions(text) + val result = findReferences(text) result.keys.any { text.substring(it) == permalink @@ -525,7 +521,7 @@ class MatrixRegexTest : TrixnityBaseTest() { // Parameters fun parameterTest(uri: String, params: Parameters, expected: Boolean) { - val mentions = findMentions(uri) + val mentions = findReferences(uri) mentions.values.forEach { val parameters = when (it) { @@ -614,13 +610,6 @@ class MatrixRegexTest : TrixnityBaseTest() { expected = "matrix:u/user:example.org", actual = content.substring(86..110), ) - assertEquals( - expected = mapOf( - 30..78 to Reference.User(UserId("@user:example.org"), parametersOf("action", "chat")), - 86..110 to Reference.User(UserId("@user:example.org")), - ), - actual = findLinkMentions(content) - ) // Ids assertEquals( expected = "@user:example.org", @@ -630,21 +619,14 @@ class MatrixRegexTest : TrixnityBaseTest() { expected = "@user:example.org", actual = content.substring(50..66), ) - assertEquals( - expected = mapOf( - 6..22 to Reference.User(UserId("@user:example.org")), - 50..66 to Reference.User(UserId("@user:example.org")), - ), - actual = findIdMentions(content) - ) - // Combined + // References assertEquals( expected = mapOf( 30..78 to Reference.User(UserId("@user:example.org"), parametersOf("action", "chat")), 86..110 to Reference.User(UserId("@user:example.org")), 6..22 to Reference.User(UserId("@user:example.org")), ), - actual = findMentions(content) + actual = findReferences(content) ) assertEquals( expected = mapOf( @@ -652,7 +634,7 @@ class MatrixRegexTest : TrixnityBaseTest() { 92..171 to Reference.Room(roomId=RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), parameters=parametersOf("via" to listOf("matrix.org", "imbitbu.de"))), 199..323 to Reference.Event(roomId=RoomId("!WvOltebgJfkgHzhfpW:matrix.org"), eventId=EventId("\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A"), parameters=parametersOf("via" to listOf("matrix.org", "imbitbu.de"))), ), - actual = findMentions( + actual = findReferences( "Some Username: This is a user mention
" + "https://matrix.to/#/!WvOltebgJfkgHzhfpW:matrix.org?via=matrix.org&via=imbitbu.de This is a room mention
" + "https://matrix.to/#/!WvOltebgJfkgHzhfpW:matrix.org/\$KoEcMwZKqGpCeuMjAmt9zvmWgO72f7hDFkvfBMS479A?via=matrix.org&via=imbitbu.de This is an event mention" @@ -664,9 +646,9 @@ class MatrixRegexTest : TrixnityBaseTest() { fun `finds regular links`() { assertEquals( expected = mapOf( - 19..65 to "https://en.wikipedia.org/wiki/Matrix_(protocol)", + 19..65 to Reference.Link("https://en.wikipedia.org/wiki/Matrix_(protocol)"), ), - actual = findLinks( + actual = findReferences( "I saw that online (https://en.wikipedia.org/wiki/Matrix_(protocol)), neat eh?" ) ) diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/ParseLinkTest.kt similarity index 63% rename from trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt rename to trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/ParseLinkTest.kt index 8c8c35120..0d5f342a6 100644 --- a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/MatrixLinkTest.kt +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/ParseLinkTest.kt @@ -1,43 +1,41 @@ -package net.folivo.trixnity.core +package net.folivo.trixnity.core.util import io.ktor.http.* import net.folivo.trixnity.core.model.EventId import net.folivo.trixnity.core.model.RoomAliasId import net.folivo.trixnity.core.model.RoomId import net.folivo.trixnity.core.model.UserId -import net.folivo.trixnity.core.util.MatrixLinks -import net.folivo.trixnity.core.util.Reference import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNull -class MatrixLinkTest { +class ParseLinkTest { @Test fun `fails on invalid links`() { - assertNull(MatrixLinks.parse("invalid-link")) - assertNull(MatrixLinks.parse("https://example.com")) - assertNull(MatrixLinks.parse("https://matrix.to/#/disclaimer/")) - assertNull(MatrixLinks.parse("https://matrix.to/robots.txt")) - assertNull(MatrixLinks.parse("matrix:group/group:example.com")) + assertNull(Reference.parseLink("invalid-link")) + assertNull(Reference.parseLink("https://example.com")) + assertNull(Reference.parseLink("https://matrix.to/#/disclaimer/")) + assertNull(Reference.parseLink("https://matrix.to/robots.txt")) + assertNull(Reference.parseLink("matrix:group/group:example.com")) } @Test fun `parses matrixto user links`() { assertEquals( expected = Reference.User(UserId("@user:example.com")), - actual = MatrixLinks.parse("https://matrix.to/#/@user:example.com") + actual = Reference.parseLink("https://matrix.to/#/@user:example.com") ) assertEquals( expected = Reference.User(UserId("@user:example.com")), - actual = MatrixLinks.parse("https://matrix.to/#%2F%40user%3Aexample.com") + actual = Reference.parseLink("https://matrix.to/#%2F%40user%3Aexample.com") ) assertEquals( expected = Reference.User(UserId("@user:example.com"), parametersOf("action", "chat")), - actual = MatrixLinks.parse("https://matrix.to/#/@user:example.com?action=chat") + actual = Reference.parseLink("https://matrix.to/#/@user:example.com?action=chat") ) assertEquals( expected = Reference.User(UserId("@user:example.com"), parametersOf("action", "chat")), - actual = MatrixLinks.parse("https://matrix.to/#%2F%40user%3Aexample.com%3Faction=chat") + actual = Reference.parseLink("https://matrix.to/#%2F%40user%3Aexample.com%3Faction=chat") ) } @@ -45,11 +43,11 @@ class MatrixLinkTest { fun `parses matrix protocol user links`() { assertEquals( expected = Reference.User(UserId("@user:example.com")), - actual = MatrixLinks.parse("matrix:u/user:example.com") + actual = Reference.parseLink("matrix:u/user:example.com") ) assertEquals( expected = Reference.User(UserId("@user:example.com"), parametersOf("action", "chat")), - actual = MatrixLinks.parse("matrix:u/user:example.com?action=chat") + actual = Reference.parseLink("matrix:u/user:example.com?action=chat") ) } @@ -57,25 +55,25 @@ class MatrixLinkTest { fun `parses matrixto room alias links`() { assertEquals( expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org")), - actual = MatrixLinks.parse("https://matrix.to/#/#somewhere:example.org") + actual = Reference.parseLink("https://matrix.to/#/#somewhere:example.org") ) assertEquals( expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org")), - actual = MatrixLinks.parse("https://matrix.to/#%2F#somewhere%3Aexample.org") + actual = Reference.parseLink("https://matrix.to/#%2F#somewhere%3Aexample.org") ) assertEquals( expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#/#somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#/#somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#%2F#somewhere%3Aexample.org%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#%2F#somewhere%3Aexample.org%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") ) } @@ -83,14 +81,14 @@ class MatrixLinkTest { fun `parses matrix protocol room alias links`() { assertEquals( expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org")), - actual = MatrixLinks.parse("matrix:r/somewhere:example.org") + actual = Reference.parseLink("matrix:r/somewhere:example.org") ) assertEquals( expected = Reference.RoomAlias(RoomAliasId("#somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("matrix:r/somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("matrix:r/somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") ) } @@ -98,25 +96,25 @@ class MatrixLinkTest { fun `parses matrixto roomid links`() { assertEquals( expected = Reference.Room(RoomId("!somewhere:example.org")), - actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org") + actual = Reference.parseLink("https://matrix.to/#/!somewhere:example.org") ) assertEquals( expected = Reference.Room(RoomId("!somewhere:example.org")), - actual = MatrixLinks.parse("https://matrix.to/#%2F!somewhere%3Aexample.org") + actual = Reference.parseLink("https://matrix.to/#%2F!somewhere%3Aexample.org") ) assertEquals( expected = Reference.Room(RoomId("!somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#/!somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( expected = Reference.Room(RoomId("!somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#%2F!somewhere%3Aexample.org%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#%2F!somewhere%3Aexample.org%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") ) } @@ -124,14 +122,14 @@ class MatrixLinkTest { fun `parses matrix protocol roomid links`() { assertEquals( expected = Reference.Room(RoomId("!somewhere:example.org")), - actual = MatrixLinks.parse("matrix:roomid/somewhere:example.org") + actual = Reference.parseLink("matrix:roomid/somewhere:example.org") ) assertEquals( expected = Reference.Room(RoomId("!somewhere:example.org"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("matrix:roomid/somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("matrix:roomid/somewhere:example.org?via=example.org&action=join&via=elsewhere.ca") ) } @@ -139,25 +137,25 @@ class MatrixLinkTest { fun `parses matrixto roomid v12 links`() { assertEquals( expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") ) } @@ -165,14 +163,14 @@ class MatrixLinkTest { fun `parses matrix protocol roomid v12 links`() { assertEquals( expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Room(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) } @@ -180,47 +178,47 @@ class MatrixLinkTest { fun `parses matrixto event links`() { assertEquals( expected = Reference.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/!somewhere:example.org/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#/!somewhere%3Aexample.org/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/!somewhere%3Aexample.org/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(RoomId("!somewhere:example.org"),EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#/!somewhere:example.org/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#/!somewhere:example.org/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( expected = Reference.Event(RoomId("!somewhere:example.org"),EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#%2F!somewhere%3Aexample.org%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#%2F!somewhere%3Aexample.org%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") ) assertEquals( expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") ) } @@ -228,14 +226,14 @@ class MatrixLinkTest { fun `parses matrix protocol event links`() { assertEquals( expected = Reference.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("matrix:roomid/somewhere:example.org/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("matrix:roomid/somewhere:example.org/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(RoomId("!somewhere:example.org"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("matrix:roomid/somewhere:example.org/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("matrix:roomid/somewhere:example.org/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) } @@ -243,47 +241,47 @@ class MatrixLinkTest { fun `parses matrixto event v12 links`() { assertEquals( expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#%2F!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") ) assertEquals( expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) assertEquals( expected = Reference.Event(null, EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("https://matrix.to/#%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") + actual = Reference.parseLink("https://matrix.to/#%2F%24NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE%3Fvia%3Dexample.org%26action%3Djoin%26via%3Delsewhere.ca") ) } @@ -291,14 +289,14 @@ class MatrixLinkTest { fun `parses matrix protocol event v12 links`() { assertEquals( expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(RoomId("!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId("\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"),parametersOf( "action" to listOf("join"), "via" to listOf("example.org", "elsewhere.ca") )), - actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") + actual = Reference.parseLink("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE?via=example.org&action=join&via=elsewhere.ca") ) } @@ -315,31 +313,31 @@ class MatrixLinkTest { ) assertEquals( expected = Reference.User(UserId(UserId.sigilCharacter + longId)), - actual = MatrixLinks.parse("https://matrix.to/#/@$longId") + actual = Reference.parseLink("https://matrix.to/#/@$longId") ) assertEquals( expected = Reference.RoomAlias(RoomAliasId(RoomAliasId.sigilCharacter + longId)), - actual = MatrixLinks.parse("https://matrix.to/#/#$longId") + actual = Reference.parseLink("https://matrix.to/#/#$longId") ) assertEquals( expected = Reference.Room(RoomId(RoomId.sigilCharacter + longId)), - actual = MatrixLinks.parse("https://matrix.to/#/!$longId") + actual = Reference.parseLink("https://matrix.to/#/!$longId") ) assertEquals( expected = Reference.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("https://matrix.to/#/!$longId/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/!$longId/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(RoomId(RoomId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId(EventId.sigilCharacter + longId)), - actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/$$longId") + actual = Reference.parseLink("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/$$longId") ) assertEquals( expected = Reference.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + longId)), - actual = MatrixLinks.parse("https://matrix.to/#/!$longId/$$longId") + actual = Reference.parseLink("https://matrix.to/#/!$longId/$$longId") ) assertEquals( expected = Reference.Event(null, EventId(EventId.sigilCharacter + longId)), - actual = MatrixLinks.parse("https://matrix.to/#/$$longId") + actual = Reference.parseLink("https://matrix.to/#/$$longId") ) } @@ -356,27 +354,27 @@ class MatrixLinkTest { ) assertEquals( expected = Reference.User(UserId(UserId.sigilCharacter + longId)), - actual = MatrixLinks.parse("matrix:u/$longId") + actual = Reference.parseLink("matrix:u/$longId") ) assertEquals( expected = Reference.RoomAlias(RoomAliasId(RoomAliasId.sigilCharacter + longId)), - actual = MatrixLinks.parse("matrix:r/$longId") + actual = Reference.parseLink("matrix:r/$longId") ) assertEquals( expected = Reference.Room(RoomId(RoomId.sigilCharacter + longId)), - actual = MatrixLinks.parse("matrix:roomid/$longId") + actual = Reference.parseLink("matrix:roomid/$longId") ) assertEquals( expected = Reference.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE")), - actual = MatrixLinks.parse("matrix:roomid/$longId/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("matrix:roomid/$longId/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = Reference.Event(RoomId(RoomId.sigilCharacter + "NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE"), EventId(EventId.sigilCharacter + longId)), - actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$longId") + actual = Reference.parseLink("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$longId") ) assertEquals( expected = Reference.Event(RoomId(RoomId.sigilCharacter + longId), EventId(EventId.sigilCharacter + longId)), - actual = MatrixLinks.parse("matrix:roomid/$longId/e/$longId") + actual = Reference.parseLink("matrix:roomid/$longId/e/$longId") ) } @@ -389,31 +387,31 @@ class MatrixLinkTest { ) assertEquals( expected = null, - actual = MatrixLinks.parse("https://matrix.to/#/@$tooLongId") + actual = Reference.parseLink("https://matrix.to/#/@$tooLongId") ) assertEquals( expected = null, - actual = MatrixLinks.parse("https://matrix.to/#/#$tooLongId") + actual = Reference.parseLink("https://matrix.to/#/#$tooLongId") ) assertEquals( expected = null, - actual = MatrixLinks.parse("https://matrix.to/#/!$tooLongId") + actual = Reference.parseLink("https://matrix.to/#/!$tooLongId") ) assertEquals( expected = null, - actual = MatrixLinks.parse("https://matrix.to/#/!$tooLongId/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("https://matrix.to/#/!$tooLongId/\$NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = null, - actual = MatrixLinks.parse("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$$tooLongId") + actual = Reference.parseLink("https://matrix.to/#/!NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$$tooLongId") ) assertEquals( expected = null, - actual = MatrixLinks.parse("https://matrix.to/#/!$tooLongId/$$tooLongId") + actual = Reference.parseLink("https://matrix.to/#/!$tooLongId/$$tooLongId") ) assertEquals( expected = null, - actual = MatrixLinks.parse("https://matrix.to/#/$$tooLongId") + actual = Reference.parseLink("https://matrix.to/#/$$tooLongId") ) } @@ -426,31 +424,31 @@ class MatrixLinkTest { ) assertEquals( expected = null, - actual = MatrixLinks.parse("matrix:u/$tooLongId") + actual = Reference.parseLink("matrix:u/$tooLongId") ) assertEquals( expected = null, - actual = MatrixLinks.parse("matrix:r/$tooLongId") + actual = Reference.parseLink("matrix:r/$tooLongId") ) assertEquals( expected = null, - actual = MatrixLinks.parse("matrix:roomid/$tooLongId") + actual = Reference.parseLink("matrix:roomid/$tooLongId") ) assertEquals( expected = null, - actual = MatrixLinks.parse("matrix:roomid/$tooLongId/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") + actual = Reference.parseLink("matrix:roomid/$tooLongId/e/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE") ) assertEquals( expected = null, - actual = MatrixLinks.parse("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$tooLongId") + actual = Reference.parseLink("matrix:roomid/NXTQJLZfL7TpVrS6TcznngpZiiuwZcJXdr1ODlnT-sE/e/$tooLongId") ) assertEquals( expected = null, - actual = MatrixLinks.parse("matrix:roomid/$tooLongId/e/$tooLongId") + actual = Reference.parseLink("matrix:roomid/$tooLongId/e/$tooLongId") ) assertEquals( expected = null, - actual = MatrixLinks.parse("matrix:e/$tooLongId") + actual = Reference.parseLink("matrix:e/$tooLongId") ) } } -- GitLab From 9ac26790170d05997839741735c27423bb651aa7 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 11:21:44 +0200 Subject: [PATCH 08/12] Remove duplicate id length validation --- .../commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt index 7ca336457..3452e1598 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt @@ -74,7 +74,6 @@ sealed interface Reference { return MatrixIdRegex.autolinkId .findAll(content, startIndex = from) .filter { it.range.last < to } - .filter { it.range.last - it.range.first <= 255 } .mapNotNull { Pair(it.range, parseMatrixId(it.value) ?: return@mapNotNull null) } .toMap() } -- GitLab From 9030846fcd137f8d6a702ccbd3dcf938497dcbf6 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 11:24:04 +0200 Subject: [PATCH 09/12] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63e3e319b..a7ba2bb61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix OOM gradle dependencies tasks - Fix OOM during initial sync due to sync response parsing - Fix DNS regex +- Fixed issues with Reference/Link parsing ### Security -- GitLab From cf4755a13935a9f482d9667bdb0a4559b79fed9c Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 11:56:58 +0200 Subject: [PATCH 10/12] Cleanup reference filtering --- .../folivo/trixnity/core/util/Reference.kt | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt index 3452e1598..8fa44af91 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Reference.kt @@ -59,7 +59,7 @@ sealed interface Reference { val candidates = findLinkReferences(message).plus(findIdReferences(message)) return buildMap { for ((candidateKey, candidateValue) in candidates) { - val overlapKey = keys.find { it.intersect(candidateKey).isNotEmpty() } + val overlapKey = keys.find { candidateKey.overlaps(it) } if (overlapKey == null) { put(candidateKey, candidateValue) } else if (overlapKey.last - overlapKey.first < candidateKey.last - candidateKey.first) { @@ -70,25 +70,20 @@ sealed interface Reference { } } - private fun findIdReferences(content: String, from: Int = 0, to: Int = content.length): Map { - return MatrixIdRegex.autolinkId - .findAll(content, startIndex = from) - .filter { it.range.last < to } - .mapNotNull { Pair(it.range, parseMatrixId(it.value) ?: return@mapNotNull null) } - .toMap() + private fun findIdReferences(content: String): Map { + return MatrixIdRegex.autolinkId.findAll(content).mapNotNull { + Pair(it.range, parseMatrixId(it.value) ?: return@mapNotNull null) + }.toMap() } - private fun findLinkReferences(content: String, from: Int = 0, to: Int = content.length): Map { - return Patterns.AUTOLINK_MATRIX_URI - .findAll(content, startIndex = from) - .filter { it.range.last < to } - .associate { - val trimmedContent = it.value.trimLink() - Pair( - it.range.first.until(it.range.first + trimmedContent.length), - parseLink(trimmedContent) ?: Link(trimmedContent) - ) - } + private fun findLinkReferences(content: String): Map { + return Patterns.AUTOLINK_MATRIX_URI.findAll(content).associate { + val trimmedContent = it.value.trimLink() + Pair( + it.range.first.until(it.range.first + trimmedContent.length), + parseLink(trimmedContent) ?: Link(trimmedContent) + ) + } } private fun parseMatrixId(id: String): Reference? { @@ -202,6 +197,10 @@ sealed interface Reference { private fun String.trimLink(): String = trimEnd(',', '.', '!', '?', ':').trimParens() + private fun IntRange.overlaps(other: IntRange): Boolean { + return this.first <= other.last && other.first <= this.last + } + private val matrixProtocol = URLProtocol("matrix", 0) } } \ No newline at end of file -- GitLab From c26871b76fe0e759748c3befb81d5cfb143afa96 Mon Sep 17 00:00:00 2001 From: Janne Mareike Koschinski Date: Wed, 6 Aug 2025 19:02:00 +0200 Subject: [PATCH 11/12] Make patterns internal, not private --- .../net/folivo/trixnity/core/util/Patterns.kt | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Patterns.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Patterns.kt index 260269a8f..7231a7f30 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Patterns.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/Patterns.kt @@ -27,7 +27,7 @@ internal object Patterns { * This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py */ // language=RegExp - private const val IANA_TOP_LEVEL_DOMAINS: String = ("(?:" + internal const val IANA_TOP_LEVEL_DOMAINS: String = ("(?:" + "(?:aaa|aarp|abb|abbott|abbvie|abc|able|abogado|abudhabi|academy|accenture|accountant" + "|accountants|aco|actor|ads|adult|aeg|aero|aetna|afl|africa|agakhan|agency|aig|airbus" + "|airforce|airtel|akdn|alibaba|alipay|allfinanz|allstate|ally|alsace|alstom|amazon|americanexpress" @@ -191,7 +191,7 @@ internal object Patterns { + "|(?:zappos|zara|zero|zip|zone|zuerich|z[amw]))") // language=RegExp - private const val IP_ADDRESS = ("((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" + internal const val IP_ADDRESS = ("((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]" + "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]" + "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + "|[1-9][0-9]|[0-9]))") @@ -200,7 +200,7 @@ internal object Patterns { * Valid UCS characters defined in RFC 3987. Excludes space characters. */ // language=RegExp - private const val UCS_CHAR = + internal const val UCS_CHAR = "\u00A1-\u1FFF" + "\u200B-\u2027" + "\u202A-\u202E" + @@ -227,85 +227,85 @@ internal object Patterns { * Valid characters for IRI label defined in RFC 3987. */ // language=RegExp - private const val LABEL_CHAR = """a-zA-Z0-9$UCS_CHAR""" + internal const val LABEL_CHAR = """a-zA-Z0-9$UCS_CHAR""" /** * Valid characters for IRI TLD defined in RFC 3987. */ // language=RegExp - private const val TLD_CHAR = """a-zA-Z$UCS_CHAR""" + internal const val TLD_CHAR = """a-zA-Z$UCS_CHAR""" /** * RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets. */ // language=RegExp - private const val IRI_LABEL = """[$LABEL_CHAR](?:[${LABEL_CHAR}_\-]{0,61}[$LABEL_CHAR]){0,1}""" + internal const val IRI_LABEL = """[$LABEL_CHAR](?:[${LABEL_CHAR}_\-]{0,61}[$LABEL_CHAR]){0,1}""" /** * RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters. */ // language=RegExp - private const val PUNYCODE_TLD = """xn--[\w\-]{0,58}\w""" + internal const val PUNYCODE_TLD = """xn--[\w\-]{0,58}\w""" // language=RegExp - private const val TLD = """($PUNYCODE_TLD|[$TLD_CHAR]{2,63})""" + internal const val TLD = """($PUNYCODE_TLD|[$TLD_CHAR]{2,63})""" // language=RegExp - private const val HOST_NAME = """($IRI_LABEL\.)+$TLD""" + internal const val HOST_NAME = """($IRI_LABEL\.)+$TLD""" // language=RegExp - private const val DOMAIN_NAME = """($HOST_NAME|$IP_ADDRESS)""" + internal const val DOMAIN_NAME = """($HOST_NAME|$IP_ADDRESS)""" // language=RegExp - private const val PROTOCOL = "(?:[hH][tT][tT][pP][sS]?)://" + internal const val PROTOCOL = "(?:[hH][tT][tT][pP][sS]?)://" /* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */ // NOTE: We've modified the word boundary matcher to add (?=\s) to match trailing slashes // language=RegExp - private const val WORD_BOUNDARY = "(?:\\b|$|^|(?=\\s))" + internal const val WORD_BOUNDARY = "(?:\\b|$|^|(?=\\s))" // language=RegExp - private const val USER_INFO = ("(?:[a-zA-Z0-9$\\-_.+!*'()" + internal const val USER_INFO = ("(?:[a-zA-Z0-9$\\-_.+!*'()" + ",;?&=]|(?:%[a-fA-F0-9]{2})){1,64}(?::(?:[a-zA-Z0-9$\\-_" + ".+!*'(),;?&=]|(?:%[a-fA-F0-9]{2})){1,25})?@") // language=RegExp - private const val PORT_NUMBER = ":\\d{1,5}" + internal const val PORT_NUMBER = ":\\d{1,5}" // language=RegExp - private const val PATH_AND_QUERY = """[/?](?:(?:[$LABEL_CHAR;/?:@&=#~\-.+!*'(),_$])|(?:%[a-fA-F0-9]{2}))*""" + internal const val PATH_AND_QUERY = """[/?](?:(?:[$LABEL_CHAR;/?:@&=#~\-.+!*'(),_$])|(?:%[a-fA-F0-9]{2}))*""" /** * Regular expression that matches known TLDs and punycode TLDs */ // language=RegExp - private const val STRICT_TLD = """(?:$IANA_TOP_LEVEL_DOMAINS|$PUNYCODE_TLD)""" + internal const val STRICT_TLD = """(?:$IANA_TOP_LEVEL_DOMAINS|$PUNYCODE_TLD)""" /** * Regular expression that matches host names using [.STRICT_TLD] */ // language=RegExp - private const val STRICT_HOST_NAME = """(?:(?:$IRI_LABEL\.)+$STRICT_TLD)""" + internal const val STRICT_HOST_NAME = """(?:(?:$IRI_LABEL\.)+$STRICT_TLD)""" /** * Regular expression that matches domain names using either [.STRICT_HOST_NAME] or * [.IP_ADDRESS] */ // language=RegExp - private const val STRICT_DOMAIN_NAME = """(?:$STRICT_HOST_NAME|$IP_ADDRESS)""" + internal const val STRICT_DOMAIN_NAME = """(?:$STRICT_HOST_NAME|$IP_ADDRESS)""" /** * Regular expression that matches domain names without a TLD */ // language=RegExp - private const val RELAXED_DOMAIN_NAME = """(?:(?:$IRI_LABEL(?:\.(?=\S))?)+|$IP_ADDRESS)""" + internal const val RELAXED_DOMAIN_NAME = """(?:(?:$IRI_LABEL(?:\.(?=\S))?)+|$IP_ADDRESS)""" /** * Regular expression to match strings that do not start with a supported protocol. The TLDs * are expected to be one of the known TLDs. */ // language=RegExp - private const val WEB_URL_WITHOUT_PROTOCOL = ("(" + internal const val WEB_URL_WITHOUT_PROTOCOL = ("(" + WORD_BOUNDARY + "(? Date: Wed, 6 Aug 2025 19:02:16 +0200 Subject: [PATCH 12/12] Support unicode in autolinked ids --- .../folivo/trixnity/core/util/MatrixIdRegex.kt | 2 +- .../trixnity/core/util/FindReferencesTest.kt | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt index 0b3785e05..cdbea6b07 100644 --- a/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt +++ b/trixnity-core/src/commonMain/kotlin/net/folivo/trixnity/core/util/MatrixIdRegex.kt @@ -69,6 +69,6 @@ internal object MatrixIdRegex { * it is NOT intended to match all valid ids. */ // language=Regexp - private const val AUTOLINK_ID_PATTERN = "[@#](?:\\p{L}|\\p{N}|[\\-.=_/+])+:$DOMAIN_PATTERN" + private const val AUTOLINK_ID_PATTERN = "[@#][a-zA-Z0-9${Patterns.UCS_CHAR}\\-.=_/+]+:$DOMAIN_PATTERN" val autolinkId = AUTOLINK_ID_PATTERN.toRegex() } \ No newline at end of file diff --git a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/FindReferencesTest.kt b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/FindReferencesTest.kt index c8836c1a3..78c340a02 100644 --- a/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/FindReferencesTest.kt +++ b/trixnity-core/src/commonTest/kotlin/net/folivo/trixnity/core/util/FindReferencesTest.kt @@ -35,6 +35,24 @@ class FindReferencesTest : TrixnityBaseTest() { @Test fun shouldPassValidUserIdentifier() { userIdTest("@a9._=-/+:example.com", "a9._=-/+", "example.com", expected = true) + userIdTest("@äöüß:example.com", "äöüß", "example.com", expected = true) + // Character classes according to https://www.unicode.org/reports/tr18/#General_Category_Property + // Ll + userIdTest("@aâăǟǻɐΐαҩոᴀᴞℼ:example.com", "aâăǟǻɐΐαҩոᴀᴞℼ", "example.com", expected = true) + // Lu + userIdTest("@AÀĀƵǺǺΆАӐḀẠἈᾸℂℇΩKÅℬℭℲℳↃⱭꝄꝆꝌꝔ:example.com", "AÀĀƵǺǺΆАӐḀẠἈᾸℂℇΩKÅℬℭℲℳↃⱭꝄꝆꝌꝔ", "example.com", expected = true) + // Lt + userIdTest("@Džᾈ:example.com", "Džᾈ", "example.com", expected = true) + // Lm + userIdTest("@ʰͺՙॱៗᡃᱼᱻᴬᵃᵄᵅᶛᶴₐ々ゝꀕꙿꧏꧦꭞ:example.com", "ʰͺՙॱៗᡃᱼᱻᴬᵃᵄᵅᶛᶴₐ々ゝꀕꙿꧏꧦꭞ", "example.com", expected = true) + // Lo + userIdTest("@ªƻʔऄঀਅઅଅஃఅಀඅกກༀཀဪᆠቚᐂᚏᚼᛒខᠠᤁᬛᮮこんにちはꁊꓐꕥꕷꤕꦙꨘꪀꪵ:example.com", "ªƻʔऄঀਅઅଅஃఅಀඅกກༀཀဪᆠቚᐂᚏᚼᛒខᠠᤁᬛᮮこんにちはꁊꓐꕥꕷꤕꦙꨘꪀꪵ", "example.com", expected = true) + // Nd + userIdTest("@0०๐၀០᠐᮰᱐0:example.com", "0०๐၀០᠐᮰᱐0", "example.com", expected = true) + // Nl + userIdTest("@ᛮⅧↆꛯ:example.com", "ᛮⅧↆꛯ", "example.com", expected = true) + // No + userIdTest("@²¼൝༫፼⁰↉⑬⒀⒔⓭⓻❼➆➐㈦㉌㉓㊷꠵:example.com", "²¼൝༫፼⁰↉⑬⒀⒔⓭⓻❼➆➐㈦㉌㉓㊷꠵", "example.com", expected = true) } @Test -- GitLab