Adds a parser for naddr when composing messages and a filter to avoid thinking the a tag is a reply instead of a quote.

This commit is contained in:
Vitor Pamplona
2023-03-08 11:47:13 -05:00
parent 40a6feab8a
commit 4d7f29ba8f
4 changed files with 65 additions and 21 deletions
@@ -1,11 +1,13 @@
package com.vitorpamplona.amethyst.model package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.note.toShortenHex
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import nostr.postr.Bech32 import nostr.postr.Bech32
import nostr.postr.Persona import nostr.postr.Persona
import nostr.postr.bechToBytes import nostr.postr.bechToBytes
import nostr.postr.toHex import nostr.postr.toHex
import nostr.postr.toNpub
/** Makes the distinction between String and Hex **/ /** Makes the distinction between String and Hex **/
typealias HexKey = String typealias HexKey = String
@@ -48,7 +50,7 @@ fun decodePublicKey(key: String): ByteArray {
} }
} }
data class DirtyKeyInfo(val type: String, val keyHex: String, val restOfWord: String) data class DirtyKeyInfo(val key: Nip19.Return, val restOfWord: String)
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? { fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
var key = mightBeAKey var key = mightBeAKey
@@ -67,11 +69,30 @@ fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
val restOfWord = key.substring(63) val restOfWord = key.substring(63)
if (key.startsWith("nsec1", true)) { if (key.startsWith("nsec1", true)) {
return DirtyKeyInfo("npub", Persona(privKey = keyB32.bechToBytes()).pubKey.toHexKey(), restOfWord) // Converts to npub
val pubkey = Nip19.uriToRoute(Persona(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("npub1", true)) { } else if (key.startsWith("npub1", true)) {
return DirtyKeyInfo("npub", keyB32.bechToBytes().toHexKey(), restOfWord) val pubkey = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(pubkey, restOfWord)
} else if (key.startsWith("note1", true)) { } else if (key.startsWith("note1", true)) {
return DirtyKeyInfo("note", keyB32.bechToBytes().toHexKey(), restOfWord) val noteId = Nip19.uriToRoute(keyB32) ?: return null
return DirtyKeyInfo(noteId, restOfWord)
} else if (key.startsWith("nprofile", true)) {
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(pubkeyRelay, restOfWord)
} else if (key.startsWith("nevent", true)) {
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(noteRelayId, restOfWord)
} else if (key.startsWith("naddr1", true)) {
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
return DirtyKeyInfo(address, "") // no way to know when they address ends and dirt begins
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@@ -189,8 +189,7 @@ object LocalCache {
} }
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } + val replyTo = tagsWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, replyTo) note.loadEvent(event, author, mentions, replyTo)
@@ -235,7 +234,7 @@ object LocalCache {
} }
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } val replyTo = tagsWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
if (event.createdAt > (note.createdAt() ?: 0)) { if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, mentions, replyTo) note.loadEvent(event, author, mentions, replyTo)
@@ -329,33 +328,38 @@ object LocalCache {
if (tag != null && tag[0] == "e") { if (tag != null && tag[0] == "e") {
citations.add(tag[1]) citations.add(tag[1])
} }
if (tag != null && tag[0] == "a") {
citations.add(tag[1])
}
} catch (e: Exception) { } catch (e: Exception) {
} }
} }
return citations return citations
} }
private fun replyToWithoutCitations(event: TextNoteEvent): List<String> { private fun tagsWithoutCitations(event: TextNoteEvent): List<String> {
val repliesTo = event.replyTos() val repliesTo = event.replyTos()
if (repliesTo.isEmpty()) return repliesTo val tagAddresses = event.taggedAddresses().map { it.toTag() }
if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
val citations = findCitations(event) val citations = findCitations(event)
return if (citations.isEmpty()) { return if (citations.isEmpty()) {
repliesTo repliesTo + tagAddresses
} else { } else {
repliesTo.filter { it !in citations } repliesTo.filter { it !in citations }
} }
} }
private fun replyToWithoutCitations(event: LongTextNoteEvent): List<String> { private fun tagsWithoutCitations(event: LongTextNoteEvent): List<String> {
val repliesTo = event.replyTos() val repliesTo = event.replyTos()
if (repliesTo.isEmpty()) return repliesTo val tagAddresses = event.taggedAddresses().map { it.toTag() }
if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
val citations = findCitations(event) val citations = findCitations(event)
return if (citations.isEmpty()) { return if (citations.isEmpty()) {
repliesTo repliesTo + tagAddresses
} else { } else {
repliesTo.filter { it !in citations } repliesTo.filter { it !in citations }
} }
@@ -15,6 +15,12 @@ class LongTextNoteEvent(
) : Event(id, pubKey, createdAt, kind, tags, content, sig) { ) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
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 dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "" fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey, dTag(), null) fun address() = ATag(kind, pubKey, dTag(), null)
@@ -10,6 +10,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.* import com.vitorpamplona.amethyst.model.*
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.ui.components.isValidURL import com.vitorpamplona.amethyst.ui.components.isValidURL
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@@ -76,10 +77,15 @@ class NewPostViewModel : ViewModel() {
paragraph.split(' ').forEach { word: String -> paragraph.split(' ').forEach { word: String ->
val results = parseDirtyWordForKey(word) val results = parseDirtyWordForKey(word)
if (results?.type == "npub") { if (results?.key?.type == Nip19.Type.USER) {
addUserToMentions(LocalCache.getOrCreateUser(results.keyHex)) addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
} else if (results?.type == "note") { } else if (results?.key?.type == Nip19.Type.NOTE) {
addNoteToReplyTos(LocalCache.getOrCreateNote(results.keyHex)) addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
addNoteToReplyTos(note)
}
} }
} }
} }
@@ -88,14 +94,21 @@ class NewPostViewModel : ViewModel() {
val newMessage = message.text.split('\n').map { paragraph: String -> val newMessage = message.text.split('\n').map { paragraph: String ->
paragraph.split(' ').map { word: String -> paragraph.split(' ').map { word: String ->
val results = parseDirtyWordForKey(word) val results = parseDirtyWordForKey(word)
if (results?.type == "npub") { if (results?.key?.type == Nip19.Type.USER) {
val user = LocalCache.getOrCreateUser(results.keyHex) val user = LocalCache.getOrCreateUser(results.key.hex)
"#[${tagIndex(user)}]${results.restOfWord}" "#[${tagIndex(user)}]${results.restOfWord}"
} else if (results?.type == "note") { } else if (results?.key?.type == Nip19.Type.NOTE) {
val note = LocalCache.getOrCreateNote(results.keyHex) val note = LocalCache.getOrCreateNote(results.key.hex)
"#[${tagIndex(note)}]${results.restOfWord}" "#[${tagIndex(note)}]${results.restOfWord}"
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
if (note != null) {
"#[${tagIndex(note)}]${results.restOfWord}"
} else {
word
}
} else { } else {
word word
} }