Improves RelayHint normalization to make sure the NormalizedRelayUrls are indeed relays

This commit is contained in:
Vitor Pamplona
2025-07-03 15:45:46 -04:00
parent 64bad2b476
commit e4ebca29f4
5 changed files with 199 additions and 96 deletions

View File

@@ -20,8 +20,12 @@
*/ */
package com.vitorpamplona.quartz.nip01Core.relay.normalizer package com.vitorpamplona.quartz.nip01Core.relay.normalizer
import android.util.Log
import android.util.Log.e
import androidx.collection.LruCache import androidx.collection.LruCache
import kotlinx.coroutines.CancellationException
import org.czeal.rfc3986.URIReference import org.czeal.rfc3986.URIReference
import kotlin.contracts.ExperimentalContracts
val normalizedUrls = LruCache<String, NormalizedRelayUrl>(5000) val normalizedUrls = LruCache<String, NormalizedRelayUrl>(5000)
@@ -37,6 +41,27 @@ class RelayUrlNormalizer {
fun isOnion(url: String) = url.endsWith(".onion") || url.contains(".onion/") fun isOnion(url: String) = url.endsWith(".onion") || url.contains(".onion/")
fun isRelaySchemePrefix(url: String) = url.length > 6 && url[0] == 'w' && url[1] == 's'
fun isRelaySchemePrefixSecure(url: String) = url[2] == 's' && url[3] == ':' && url[4] == '/' && url[5] == '/' && url[6] != '/'
fun isRelaySchemePrefixInsecure(url: String) = url[2] == ':' && url[3] == '/' && url[4] == '/' && url[5] != '/'
fun isRelayUrl(url: String): Boolean {
val trimmed = url.trim().ifEmpty { return false }
// fast
if (isRelaySchemePrefix(trimmed)) {
if (isRelaySchemePrefixSecure(trimmed)) {
return true
} else if (isRelaySchemePrefixInsecure(trimmed)) {
return true
}
}
return false
}
private fun norm(url: String) = private fun norm(url: String) =
NormalizedRelayUrl( NormalizedRelayUrl(
URIReference URIReference
@@ -46,19 +71,20 @@ class RelayUrlNormalizer {
.intern(), .intern(),
) )
fun fix(url: String): String { @OptIn(ExperimentalContracts::class)
val trimmed = url.trim() fun fix(url: String): String? {
val trimmed = url.trim().ifEmpty { return null }
// fast if (trimmed.isEmpty()) return null
if (trimmed.length > 4 && trimmed[0] == 'w' && trimmed[1] == 's') {
if (trimmed[2] == 's' && trimmed[3] == ':' && trimmed[4] == '/' && trimmed[5] == '/') { // fast for good wss:// urls
return trimmed if (isRelaySchemePrefix(trimmed)) {
} else if (trimmed[2] == ':' && trimmed[3] == '/' && trimmed[4] == '/') { if (isRelaySchemePrefixSecure(trimmed) || isRelaySchemePrefixInsecure(trimmed)) {
return trimmed return trimmed
} }
} }
// fast // fast for good https:// urls
if (trimmed.length > 8 && trimmed[0] == 'h' && trimmed[1] == 't' && trimmed[2] == 't' && trimmed[3] == 'p') { if (trimmed.length > 8 && trimmed[0] == 'h' && trimmed[1] == 't' && trimmed[2] == 't' && trimmed[3] == 'p') {
if (trimmed[4] == 's' && trimmed[5] == ':' && trimmed[6] == '/' && trimmed[7] == '/') { if (trimmed[4] == 's' && trimmed[5] == ':' && trimmed[6] == '/' && trimmed[7] == '/') {
// https:// // https://
@@ -69,6 +95,12 @@ class RelayUrlNormalizer {
} }
} }
if (trimmed.contains("://")) {
// some other scheme we cannot connect to.
Log.w("RelayUrlNormalizer", "Rejected relay URL: $url")
return null
}
return if (isOnion(trimmed) || isLocalHost(trimmed)) { return if (isOnion(trimmed) || isLocalHost(trimmed)) {
"ws://$trimmed" "ws://$trimmed"
} else { } else {
@@ -80,7 +112,8 @@ class RelayUrlNormalizer {
normalizedUrls.get(url)?.let { return it } normalizedUrls.get(url)?.let { return it }
return try { return try {
val normalized = norm(fix(url)) val fixed = fix(url) ?: return NormalizedRelayUrl(url)
val normalized = norm(fixed)
normalizedUrls.put(url, normalized) normalizedUrls.put(url, normalized)
normalized normalized
} catch (e: Exception) { } catch (e: Exception) {
@@ -89,13 +122,22 @@ class RelayUrlNormalizer {
} }
fun normalizeOrNull(url: String): NormalizedRelayUrl? { fun normalizeOrNull(url: String): NormalizedRelayUrl? {
normalizedUrls.get(url)?.let { return it } if (url.isEmpty()) return null
normalizedUrls[url]?.let { return it }
return try { return try {
val normalized = norm(fix(url)) val fixed = fix(url)
normalizedUrls.put(url, normalized) if (fixed != null) {
normalized val normalized = norm(fixed)
normalizedUrls.put(url, normalized)
return normalized
} else {
Log.w("NormalizedRelayUrl", "Rejected Error $url")
null
}
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e
Log.w("NormalizedRelayUrl", "Rejected Error $url")
null null
} }
} }

View File

@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.quartz.nip01Core.tags.events package com.vitorpamplona.quartz.nip01Core.tags.events
import android.R.attr.tag
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.core.has
@@ -72,8 +73,7 @@ data class ETag(
ensure(tag[0] == TAG_NAME) { return null } ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null } ensure(tag[1].length == 64) { return null }
val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } return ETag(tag[1], pickRelayHint(tag), pickAuthor(tag))
return ETag(tag[1], hint, tag.getOrNull(3))
} }
@JvmStatic @JvmStatic
@@ -84,6 +84,32 @@ data class ETag(
return tag[1] return tag[1]
} }
// simple case ["e", "id", "relay"]
// empty tags ["e", "id", "relay", ""]
// current root ["e", "id", "relay", "marker"]
// current root ["e", "id", "relay", "marker", "pubkey"]
// empty tags ["e", "id", "relay", "", "pubkey"]
// pubkey marker ["e", "id", "relay", "pubkey"]
// pubkey marker ["e", "id", "relay", "pubkey", "marker"]
// pubkey marker ["e", "id", "pubkey"] // incorrect
// current root ["e", "id", "marker"] // incorrect
@JvmStatic
private fun pickRelayHint(tag: Array<String>): NormalizedRelayUrl? {
if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2])
if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3])
if (tag.has(4) && tag[4].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[4])) return RelayUrlNormalizer.normalizeOrNull(tag[4])
return null
}
@JvmStatic
private fun pickAuthor(tag: Array<String>): HexKey? {
if (tag.has(2) && tag[2].length == 64) return tag[2]
if (tag.has(3) && tag[3].length == 64) return tag[3]
if (tag.has(4) && tag[4].length == 64) return tag[4]
return null
}
@JvmStatic @JvmStatic
fun parseAsHint(tag: Array<String>): EventIdHint? { fun parseAsHint(tag: Array<String>): EventIdHint? {
ensure(tag.has(2)) { return null } ensure(tag.has(2)) { return null }
@@ -91,7 +117,8 @@ data class ETag(
ensure(tag[1].length == 64) { return null } ensure(tag[1].length == 64) { return null }
ensure(tag[2].isNotEmpty()) { return null } ensure(tag[2].isNotEmpty()) { return null }
val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) val hint = pickRelayHint(tag)
ensure(hint != null) { return null } ensure(hint != null) { return null }
return EventIdHint(tag[1], hint) return EventIdHint(tag[1], hint)

View File

@@ -66,11 +66,16 @@ data class PTag(
ensure(tag[0] == TAG_NAME) { return null } ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null } ensure(tag[1].length == 64) { return null }
val hint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } val hint = pickRelayHint(tag)
return PTag(tag[1], hint) return PTag(tag[1], hint)
} }
private fun pickRelayHint(tag: Array<String>): NormalizedRelayUrl? {
if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2])
return null
}
@JvmStatic @JvmStatic
fun parseKey(tag: Array<String>): HexKey? { fun parseKey(tag: Array<String>): HexKey? {
ensure(tag.has(1)) { return null } ensure(tag.has(1)) { return null }
@@ -89,10 +94,11 @@ data class PTag(
ensure(tag[1].length == 64) { return null } ensure(tag[1].length == 64) { return null }
ensure(tag[2].isNotEmpty()) { return null } ensure(tag[2].isNotEmpty()) { return null }
val normalized = RelayUrlNormalizer.normalizeOrNull(tag[2]) val hint = pickRelayHint(tag)
ensure(normalized != null) { return null }
return PubKeyHint(tag[1], normalized) ensure(hint != null) { return null }
return PubKeyHint(tag[1], hint)
} }
@JvmStatic @JvmStatic

View File

@@ -20,12 +20,14 @@
*/ */
package com.vitorpamplona.quartz.nip10Notes.tags package com.vitorpamplona.quartz.nip10Notes.tags
import android.R.attr.tag
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer.Companion.isRelayUrl
import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag import com.vitorpamplona.quartz.nip01Core.tags.events.GenericETag
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.utils.arrayOfNotNull import com.vitorpamplona.quartz.utils.arrayOfNotNull
@@ -87,67 +89,71 @@ data class MarkedETag(
@JvmStatic @JvmStatic
fun parse(tag: Array<String>): MarkedETag? { fun parse(tag: Array<String>): MarkedETag? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null ensure(tag.has(2)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
ensure(tag[2].isNotEmpty()) { return null }
return MarkedETag( return MarkedETag(
tag[ORDER_EVT_ID], eventId = tag[1],
RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), relayHint = pickRelayHint(tag),
MARKER.parse(tag[ORDER_MARKER]), marker = pickMarker(tag),
tag.getOrNull(ORDER_PUBKEY), authorPubKeyHex = pickAuthor(tag),
) )
} }
@JvmStatic @JvmStatic
fun parseId(tag: Array<String>): HexKey? { fun parseId(tag: Array<String>): HexKey? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null ensure(tag.has(2)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
return tag[ORDER_EVT_ID] return tag[1]
}
// simple case ["e", "id", "relay"]
// empty tags ["e", "id", "relay", ""]
// current root ["e", "id", "relay", "marker"]
// current root ["e", "id", "relay", "marker", "pubkey"]
// empty tags ["e", "id", "relay", "", "pubkey"]
// pubkey marker ["e", "id", "relay", "pubkey"]
// pubkey marker ["e", "id", "relay", "pubkey", "marker"]
// pubkey marker ["e", "id", "pubkey"] // incorrect
// current root ["e", "id", "marker"] // incorrect
@JvmStatic
private fun pickRelayHint(tag: Array<String>): NormalizedRelayUrl? {
if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2])
if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3])
if (tag.has(4) && tag[4].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[4])) return RelayUrlNormalizer.normalizeOrNull(tag[4])
return null
}
@JvmStatic
private fun pickAuthor(tag: Array<String>): HexKey? {
if (tag.has(3) && tag[3].length == 64) return tag[3]
if (tag.has(4) && tag[4].length == 64) return tag[4]
if (tag.has(2) && tag[2].length == 64) return tag[2]
return null
}
@JvmStatic
private fun pickMarker(tag: Array<String>): MARKER? {
if (tag.has(3)) MARKER.parse(tag[3])?.let { return it }
if (tag.has(4)) MARKER.parse(tag[4])?.let { return it }
if (tag.has(2)) MARKER.parse(tag[2])?.let { return it }
return null
} }
@JvmStatic @JvmStatic
fun parseAllThreadTags(tag: Array<String>): MarkedETag? = fun parseAllThreadTags(tag: Array<String>): MarkedETag? =
if (tag.size >= 2 && tag[0] == TAG_NAME) { if (tag.size >= 2 && tag[0] == TAG_NAME) {
if (tag.size <= 3) { MarkedETag(
// simple case ["e", "id", "relay"] eventId = tag[1],
MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, null, null) relayHint = pickRelayHint(tag),
} else if (tag.size == 4) { marker = pickMarker(tag),
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) authorPubKeyHex = pickAuthor(tag),
if (tag[3].isEmpty()) { )
// empty tags ["e", "id", "relay", ""]
MarkedETag(tag[1], relayHint, null, null)
} else if (tag[3].length == 64) {
// updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"]
MarkedETag(tag[1], relayHint, null, tag[3])
} else if (tag[3] == MARKER.ROOT.code) {
// corrent root ["e", "id", "relay", "root"]
MarkedETag(tag[1], relayHint, MARKER.ROOT)
} else if (tag[3] == MARKER.REPLY.code) {
// correct reply ["e", "id", "relay", "reply"]
MarkedETag(tag[1], relayHint, MARKER.REPLY)
} else {
// ignore "mention" and "fork" markers
null
}
} else {
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2])
// tag.size >= 5
if (tag[3].isEmpty()) {
// empty tags ["e", "id", "relay", "", "pubkey"]
MarkedETag(tag[1], relayHint, null, tag[4])
} else if (tag[3].length == 64) {
// updated case with pubkey instead of marker ["e", "id", "relay", "pubkey"]
MarkedETag(tag[1], relayHint, null, tag[3])
} else if (tag[3] == MARKER.ROOT.code) {
// corrent root ["e", "id", "relay", "root"]
MarkedETag(tag[1], relayHint, MARKER.ROOT, tag[4])
} else if (tag[3] == MARKER.REPLY.code) {
// correct reply ["e", "id", "relay", "reply"]
MarkedETag(tag[1], relayHint, MARKER.REPLY, tag[4])
} else {
// ignore "mention" and "fork" markers
null
}
}
} else { } else {
null null
} }
@@ -187,9 +193,8 @@ data class MarkedETag(
ensure(tag.has(2)) { return null } ensure(tag.has(2)) { return null }
ensure(tag[0] == TAG_NAME) { return null } ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null } ensure(tag[1].length == 64) { return null }
ensure(tag[2].isNotEmpty()) { return null }
val hint = RelayUrlNormalizer.normalizeOrNull(tag[2]) val hint = pickRelayHint(tag)
ensure(hint != null) { return null } ensure(hint != null) { return null }
return EventIdHint(tag[1], hint) return EventIdHint(tag[1], hint)
@@ -197,15 +202,18 @@ data class MarkedETag(
@JvmStatic @JvmStatic
fun parseRoot(tag: Array<String>): MarkedETag? { fun parseRoot(tag: Array<String>): MarkedETag? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null ensure(tag.has(3)) { return null }
if (tag[ORDER_MARKER] != MARKER.ROOT.code) return null ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].length == 64) { return null }
val marker = pickMarker(tag)
ensure(marker == MARKER.ROOT) { return null }
// ["e", id hex, relay hint, marker, pubkey]
return MarkedETag( return MarkedETag(
eventId = tag[ORDER_EVT_ID], eventId = tag[1],
relayHint = RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), relayHint = pickRelayHint(tag),
marker = MARKER.ROOT, marker = marker,
authorPubKeyHex = tag.getOrNull(ORDER_PUBKEY), authorPubKeyHex = pickAuthor(tag),
) )
} }
@@ -215,23 +223,25 @@ data class MarkedETag(
@JvmStatic @JvmStatic
fun parseUnmarkedRoot(tag: Array<String>): MarkedETag? = fun parseUnmarkedRoot(tag: Array<String>): MarkedETag? =
if (tag.size in 2..3 && tag[0] == TAG_NAME) { if (tag.size in 2..3 && tag[0] == TAG_NAME) {
MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, MARKER.ROOT) MarkedETag(tag[1], pickRelayHint(tag), MARKER.ROOT)
} else { } else {
null null
} }
@JvmStatic @JvmStatic
fun parseReply(tag: Array<String>): MarkedETag? { fun parseReply(tag: Array<String>): MarkedETag? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null ensure(tag.has(3)) { return null }
if (tag[ORDER_MARKER] != MARKER.REPLY.code) return null ensure(tag[0] == TAG_NAME) { return null }
// ["e", id hex, relay hint, marker, pubkey] ensure(tag[1].length == 64) { return null }
val marker = pickMarker(tag)
ensure(marker == MARKER.REPLY) { return null }
return MarkedETag( return MarkedETag(
tag[ORDER_EVT_ID], eventId = tag[1],
RelayUrlNormalizer.normalizeOrNull(tag[ORDER_RELAY]), relayHint = pickRelayHint(tag),
MARKER.REPLY, marker = marker,
tag.getOrNull( authorPubKeyHex = pickAuthor(tag),
ORDER_PUBKEY,
),
) )
} }
@@ -241,17 +251,21 @@ data class MarkedETag(
@JvmStatic @JvmStatic
fun parseUnmarkedReply(tag: Array<String>): MarkedETag? = fun parseUnmarkedReply(tag: Array<String>): MarkedETag? =
if (tag.size in 2..3 && tag[0] == TAG_NAME) { if (tag.size in 2..3 && tag[0] == TAG_NAME) {
MarkedETag(tag[1], tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }, MARKER.REPLY) MarkedETag(tag[1], pickRelayHint(tag), MARKER.REPLY)
} else { } else {
null null
} }
@JvmStatic @JvmStatic
fun parseRootId(tag: Array<String>): HexKey? { fun parseRootId(tag: Array<String>): HexKey? {
if (tag.size < TAG_SIZE || tag[0] != TAG_NAME) return null ensure(tag.has(3)) { return null }
if (tag[ORDER_MARKER] != MARKER.ROOT.code) return null ensure(tag[0] == TAG_NAME) { return null }
// ["e", id hex, relay hint, marker, pubkey] ensure(tag[1].length == 64) { return null }
return tag[ORDER_EVT_ID]
val marker = pickMarker(tag)
ensure(marker == MARKER.ROOT) { return null }
return tag[1]
} }
@JvmStatic @JvmStatic

View File

@@ -20,9 +20,11 @@
*/ */
package com.vitorpamplona.quartz.nip18Reposts.quotes package com.vitorpamplona.quartz.nip18Reposts.quotes
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.has import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.utils.ensure import com.vitorpamplona.quartz.utils.ensure
@@ -38,16 +40,28 @@ interface QTag {
ensure(tag.has(1)) { return null } ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null } ensure(tag[0] == TAG_NAME) { return null }
val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) } val relayHint = pickRelayHint(tag)
return if (tag[1].length == 64) { return if (tag[1].length == 64) {
QEventTag(tag[1], relayHint, tag.getOrNull(3)) QEventTag(tag[1], relayHint, pickAuthor(tag))
} else { } else {
val address = Address.parse(tag[1]) ?: return null val address = Address.parse(tag[1]) ?: return null
QAddressableTag(address, relayHint) QAddressableTag(address, relayHint)
} }
} }
private fun pickRelayHint(tag: Array<String>): NormalizedRelayUrl? {
if (tag.has(2) && tag[2].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[2])) return RelayUrlNormalizer.normalizeOrNull(tag[2])
if (tag.has(3) && tag[3].length > 7 && RelayUrlNormalizer.isRelayUrl(tag[3])) return RelayUrlNormalizer.normalizeOrNull(tag[3])
return null
}
private fun pickAuthor(tag: Array<String>): HexKey? {
if (tag.has(2) && tag[2].length == 64) return tag[2]
if (tag.has(3) && tag[3].length == 64) return tag[3]
return null
}
@JvmStatic @JvmStatic
fun parseKey(tag: Array<String>): String? { fun parseKey(tag: Array<String>): String? {
ensure(tag.has(1)) { return null } ensure(tag.has(1)) { return null }
@@ -62,7 +76,7 @@ interface QTag {
ensure(tag[1].length == 64) { return null } ensure(tag[1].length == 64) { return null }
ensure(tag[2].isNotEmpty()) { return null } ensure(tag[2].isNotEmpty()) { return null }
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) val relayHint = pickRelayHint(tag)
ensure(relayHint != null) { return null } ensure(relayHint != null) { return null }
return EventIdHint(tag[1], relayHint) return EventIdHint(tag[1], relayHint)
@@ -76,7 +90,7 @@ interface QTag {
ensure(tag[2].isNotEmpty()) { return null } ensure(tag[2].isNotEmpty()) { return null }
ensure(!tag[1].contains(':')) { return null } ensure(!tag[1].contains(':')) { return null }
val relayHint = RelayUrlNormalizer.normalizeOrNull(tag[2]) val relayHint = pickRelayHint(tag)
ensure(relayHint != null) { return null } ensure(relayHint != null) { return null }
return AddressHint(tag[1], relayHint) return AddressHint(tag[1], relayHint)