1. Moves AddressableNote index from NAddr to aTag format due to the presence of a preferred relay inside NAddr, which is an optional field and should not be part of the idex.

2. Parses relay information for Addressable Notes
This commit is contained in:
Vitor Pamplona
2023-03-07 13:48:05 -05:00
parent 5add9669f2
commit 1919dc5c81
19 changed files with 150 additions and 44 deletions

View File

@@ -77,7 +77,7 @@ object LocalCache {
}
fun checkGetOrCreateNote(key: String): Note? {
if (key.startsWith("naddr1")) {
if (key.startsWith("naddr1") || key.contains(":")) {
return checkGetOrCreateAddressableNote(key)
}
return try {
@@ -120,7 +120,7 @@ object LocalCache {
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? {
return try {
val addr = ATag.parse(key)
val addr = ATag.parse(key, null) // relay doesn't matter for the index.
if (addr != null)
getOrCreateAddressableNote(addr)
else
@@ -133,10 +133,12 @@ object LocalCache {
@Synchronized
fun getOrCreateAddressableNote(key: ATag): AddressableNote {
return addressables[key.toNAddr()] ?: run {
// we can't use naddr here because naddr might include relay info and
// the preferred relay should not be part of the index.
return addressables[key.toTag()] ?: run {
val answer = AddressableNote(key)
answer.author = checkGetOrCreateUser(key.pubKeyHex)
addressables.put(key.toNAddr(), answer)
addressables.put(key.toTag(), answer)
answer
}
}

View File

@@ -24,7 +24,7 @@ import kotlinx.coroutines.withContext
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
class AddressableNote(val address: ATag): Note(address.toNAddr()) {
class AddressableNote(val address: ATag): Note(address.toTag()) {
override fun idNote() = address.toNAddr()
override fun idDisplayNote() = idNote().toShortenHex()
override fun address() = address

View File

@@ -35,8 +35,8 @@ class ThreadAssembler {
@OptIn(ExperimentalTime::class)
fun findThreadFor(noteId: String): Set<Note> {
val (result, elapsed) = measureTimedValue {
val note = if (noteId.startsWith("naddr")) {
val aTag = ATag.parse(noteId)
val note = if (noteId.contains(":")) {
val aTag = ATag.parse(noteId, null)
if (aTag != null)
LocalCache.getOrCreateAddressableNote(aTag)
else

View File

@@ -11,7 +11,7 @@ class Nip19 {
USER, NOTE, RELAY, ADDRESS
}
data class Return(val type: Type, val hex: String)
data class Return(val type: Type, val hex: String, val relay: String?)
fun uriToRoute(uri: String?): Return? {
try {
@@ -39,29 +39,39 @@ class Nip19 {
}
private fun npub(bytes: ByteArray): Return {
return Return(Type.USER, bytes.toHexKey())
return Return(Type.USER, bytes.toHexKey(), null)
}
private fun note(bytes: ByteArray): Return {
return Return(Type.NOTE, bytes.toHexKey());
return Return(Type.NOTE, bytes.toHexKey(), null);
}
private fun nprofile(bytes: ByteArray): Return? {
val hex = parseTLV(bytes)
.get(NIP19TLVTypes.SPECIAL.id)
val tlv = parseTLV(bytes)
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.toHexKey() ?: return null
return Return(Type.USER, hex)
val relay = tlv.get(NIP19TLVTypes.RELAY.id)
?.get(0)
?.toString(Charsets.UTF_8)
return Return(Type.USER, hex, relay)
}
private fun nevent(bytes: ByteArray): Return? {
val hex = parseTLV(bytes)
.get(NIP19TLVTypes.SPECIAL.id)
val tlv = parseTLV(bytes)
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.toHexKey() ?: return null
return Return(Type.USER, hex)
val relay = tlv.get(NIP19TLVTypes.RELAY.id)
?.get(0)
?.toString(Charsets.UTF_8)
return Return(Type.USER, hex, relay)
}
private fun nrelay(bytes: ByteArray): Return? {
@@ -70,7 +80,7 @@ class Nip19 {
?.get(0)
?.toString(Charsets.UTF_8) ?: return null
return Return(Type.RELAY, relayUrl)
return Return(Type.RELAY, relayUrl, null)
}
private fun naddr(bytes: ByteArray): Return? {
@@ -92,7 +102,7 @@ class Nip19 {
?.get(0)
?.let { toInt32(it) }
return Return(Type.ADDRESS, "$kind:$author:$d")
return Return(Type.ADDRESS, "$kind:$author:$d", relay)
}
}

View File

@@ -84,7 +84,7 @@ object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(BadgeProfilesEvent.kind),
tags = mapOf("p" to listOf(it.pubkeyHex)),
authors = listOf(it.pubkeyHex),
limit = 1
)
)

View File

@@ -11,35 +11,41 @@ import nostr.postr.Bech32
import nostr.postr.bechToBytes
import nostr.postr.toByteArray
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val relay: String?) {
fun toTag() = "$kind:$pubKeyHex:$dTag"
fun toNAddr(): String {
val kind = kind.toByteArray()
val addr = pubKeyHex.toByteArray()
val author = pubKeyHex.toByteArray()
val dTag = dTag.toByteArray(Charsets.UTF_8)
val relay = relay?.toByteArray(Charsets.UTF_8)
val fullArray =
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag +
byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr +
var fullArray =
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag
if (relay != null)
fullArray = fullArray + byteArrayOf(NIP19TLVTypes.RELAY.id, relay.size.toByte()) + relay
fullArray = fullArray +
byteArrayOf(NIP19TLVTypes.AUTHOR.id, author.size.toByte()) + author +
byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind
return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32)
}
companion object {
fun parse(address: String): ATag? {
fun parse(address: String, relay: String?): ATag? {
return if (address.startsWith("naddr") || address.startsWith("nostr:naddr"))
parseNAddr(address)
else
parseAtag(address)
parseAtag(address, relay)
}
fun parseAtag(atag: String): ATag? {
fun parseAtag(atag: String, relay: String?): ATag? {
return try {
val parts = atag.split(":")
Hex.decode(parts[1])
ATag(parts[0].toInt(), parts[1], parts[2])
ATag(parts[0].toInt(), parts[1], parts[2], relay)
} catch (t: Throwable) {
Log.w("ATag", "Error parsing A Tag: ${atag}: ${t.message}")
null
@@ -58,7 +64,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) }
if (kind != null && author != null)
return ATag(kind, author, d)
return ATag(kind, author, d, relay)
}
} catch (e: Throwable) {

View File

@@ -14,7 +14,12 @@ class BadgeAwardEvent(
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun awardees() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun awardDefinition() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
fun awardDefinition() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object {
const val kind = 8

View File

@@ -14,7 +14,7 @@ class BadgeDefinitionEvent(
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey, dTag())
fun address() = ATag(kind, pubKey, dTag(), null)
fun name() = tags.filter { it.firstOrNull() == "name" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun thumb() = tags.filter { it.firstOrNull() == "thumb" }.mapNotNull { it.getOrNull(1) }.firstOrNull()

View File

@@ -11,10 +11,15 @@ class BadgeProfilesEvent(
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey, dTag())
fun address() = ATag(kind, pubKey, dTag(), null)
companion object {
const val kind = 30008

View File

@@ -24,8 +24,12 @@ class LnZapEvent(
override fun taggedAddresses(): List<ATag> = tags
.filter { it.firstOrNull() == "a" }
.mapNotNull { it.getOrNull(1) }
.mapNotNull { ATag.parse(it) }
.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
override fun amount(): BigDecimal? {
return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }

View File

@@ -16,7 +16,12 @@ class LnZapRequestEvent (
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object {
const val kind = 9734

View File

@@ -17,7 +17,7 @@ class LongTextNoteEvent(
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey, dTag())
fun address() = ATag(kind, pubKey, dTag(), null)
fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()

View File

@@ -17,7 +17,12 @@ class ReactionEvent (
fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object {
const val kind = 7

View File

@@ -48,7 +48,12 @@ class ReportEvent (
)
}
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
companion object {
const val kind = 1984

View File

@@ -18,7 +18,12 @@ class RepostEvent (
fun boostedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun originalAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
fun containedPost() = try {
fromJson(content, Client.lenient)

View File

@@ -14,7 +14,13 @@ class TextNoteEvent(
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
companion object {

View File

@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
@@ -31,7 +32,24 @@ fun ClickableRoute(
onClick = { navController.navigate(route) },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
} else {
} else if (nip19.type == Nip19.Type.ADDRESS) {
val noteBase = LocalCache.checkGetOrCreateAddressableNote(nip19.hex)
if (noteBase == null) {
Text(
"@${nip19.hex} "
)
} else {
val noteState by noteBase.live().metadata.observeAsState()
val note = noteState?.note ?: return
ClickableText(
text = AnnotatedString("@${note.idDisplayNote()} "),
onClick = { navController.navigate("Note/${nip19.hex}") },
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
}
} else if (nip19.type == Nip19.Type.NOTE) {
val noteBase = LocalCache.getOrCreateNote(nip19.hex)
val noteState by noteBase.live().metadata.observeAsState()
val note = noteState?.note ?: return
@@ -55,5 +73,9 @@ fun ClickableRoute(
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
)
}
} else {
Text(
"@${nip19.hex} "
)
}
}

View File

@@ -200,11 +200,13 @@ private fun isArabic(text: String): Boolean {
fun isBechLink(word: String): Boolean {
return word.startsWith("nostr:", true)
|| word.startsWith("npub1", true)
|| word.startsWith("naddr1", true)
|| word.startsWith("note1", true)
|| word.startsWith("nprofile1", true)
|| word.startsWith("nevent1", true)
|| word.startsWith("@npub1", true)
|| word.startsWith("@note1", true)
|| word.startsWith("@addr1", true)
|| word.startsWith("@nprofile1", true)
|| word.startsWith("@nevent1", true)
}

View File

@@ -18,15 +18,39 @@ class NIP19ParserTest {
assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex)
}
@Test
fun nAddrParse3() {
val result = Nip19().uriToRoute("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38")
assertEquals(Nip19.Type.ADDRESS, result?.type)
assertEquals("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", result?.hex)
assertEquals("wss://relay.damus.io", result?.relay)
}
@Test
fun nAddrATagParse3() {
val address = ATag.parse("30023:d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193:89de7920", "wss://relay.damus.io")
assertEquals(30023, address?.kind)
assertEquals("d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", address?.pubKeyHex)
assertEquals("89de7920", address?.dTag)
assertEquals("wss://relay.damus.io" , address?.relay)
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address?.toNAddr())
}
@Test
fun nAddrFormatter() {
val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "" )
val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "", null)
assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr())
}
@Test
fun nAddrFormatter2() {
val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard" )
val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard", null)
assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr())
}
@Test
fun nAddrFormatter3() {
val address = ATag(30023, "d1e60465c2b777325e9133f2100d2bb31416dca810f54a1d95665621c5dee193", "89de7920", "wss://relay.damus.io")
assertEquals("naddr1qqyrswtyv5mnjv3sqy28wumn8ghj7un9d3shjtnyv9kh2uewd9hsygx3uczxts4hwue9ayfn7ggq62anzstde2qs749pm9tx2csuthhpjvpsgqqqw4rs8pmj38", address.toNAddr())
}
}