Support for Replaceable Events (NIP-33)

pull/181/head
Vitor Pamplona 2023-03-03 11:35:29 -05:00
rodzic 83f46f1a66
commit f4d5785710
43 zmienionych plików z 533 dodań i 371 usunięć

Wyświetl plik

@ -27,6 +27,8 @@ Amethyst brings the best social network to your Android phone. Just insert your
- [x] URI Support (NIP-21)
- [x] Event Deletion (NIP-09: like, boost, text notes and reports)
- [x] Identity Verification (NIP-05)
- [x] Long-form Content (NIP-23)
- [x] Parameterized Replaceable Events (NIP-33)
- [ ] Local Database
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
@ -36,7 +38,6 @@ Amethyst brings the best social network to your Android phone. Just insert your
- [ ] Generic Tags (NIP-12)
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
- [ ] Events with a Subject (NIP-14)
- [ ] Long-form Content (NIP-23)
- [ ] Online Relay Search (NIP-50)
- [ ] Workspaces
- [ ] Expiration Support (NIP-40)

Wyświetl plik

@ -290,13 +290,13 @@ class Account(
val repliesToHex = replyTo?.map { it.idHex }
val mentionsHex = mentions?.map { it.pubkeyHex }
val addressesHex = replyTo?.mapNotNull { it.address() }
val addresses = replyTo?.mapNotNull { it.address() }
val signedEvent = TextNoteEvent.create(
msg = message,
replyTos = repliesToHex,
mentions = mentionsHex,
addresses = addressesHex,
addresses = addresses,
privateKey = loggedIn.privKey!!
)
Client.send(signedEvent)

Wyświetl plik

@ -51,7 +51,7 @@ class Channel(val idHex: String) {
fun pruneOldAndHiddenMessages(account: Account): Set<Note> {
val important = notes.values
.filter { it.author?.let { it1 -> account.isHidden(it1) } == false }
.sortedBy { it.event?.createdAt }
.sortedBy { it.createdAt() }
.reversed()
.take(1000)
.toSet()

Wyświetl plik

@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.service.model.ATag
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
@ -52,6 +53,7 @@ object LocalCache {
val users = ConcurrentHashMap<HexKey, User>()
val notes = ConcurrentHashMap<HexKey, Note>()
val channels = ConcurrentHashMap<HexKey, Channel>()
val addressables = ConcurrentHashMap<String, AddressableNote>()
fun checkGetOrCreateUser(key: String): User? {
return try {
@ -111,6 +113,29 @@ object LocalCache {
}
}
fun checkGetOrCreateAddressableNote(key: String): AddressableNote? {
return try {
val addr = ATag.parse(key)
if (addr != null)
getOrCreateAddressableNote(addr)
else
null
} catch (e: IllegalArgumentException) {
Log.e("LocalCache", "Invalid Key to create channel: $key", e)
null
}
}
@Synchronized
fun getOrCreateAddressableNote(key: ATag): AddressableNote {
return addressables[key.toNAddr()] ?: run {
val answer = AddressableNote(key)
answer.author = checkGetOrCreateUser(key.pubKeyHex)
addressables.put(key.toNAddr(), answer)
answer
}
}
fun consume(event: MetadataEvent) {
// new event
@ -159,8 +184,9 @@ object LocalCache {
// Already processed this event.
if (note.event != null) return
val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, replyTo)
@ -193,7 +219,7 @@ object LocalCache {
return
}
val note = getOrCreateNote(event.id.toHex())
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey.toHexKey())
if (relay != null) {
@ -202,27 +228,26 @@ object LocalCache {
}
// Already processed this event.
if (note.event != null) return
if (note.event?.id?.toHex() == event.id.toHex()) return
val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
note.loadEvent(event, author, mentions, replyTo)
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, mentions, replyTo)
//Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content?.take(100)} ${formattedDateTime(event.createdAt)}")
author.addNote(note)
// Prepares user's profile view.
author.addLongFormNote(note)
// Adds notifications to users.
mentions.forEach {
it.addTaggedPost(note)
}
replyTo.forEach {
it.author?.addTaggedPost(note)
}
// Adds notifications to users.
mentions.forEach {
it.addTaggedPost(note)
refreshObservers()
}
replyTo.forEach {
it.author?.addTaggedPost(note)
}
refreshObservers()
}
private fun findCitations(event: Event): Set<String> {
@ -245,13 +270,13 @@ object LocalCache {
private fun replyToWithoutCitations(event: TextNoteEvent): List<String> {
val citations = findCitations(event)
return event.replyTos.filter { it !in citations }
return event.replyTos().filter { it !in citations }
}
private fun replyToWithoutCitations(event: LongTextNoteEvent): List<String> {
val citations = findCitations(event)
return event.replyTos.filter { it !in citations }
return event.replyTos().filter { it !in citations }
}
fun consume(event: RecommendRelayEvent) {
@ -378,8 +403,9 @@ object LocalCache {
//Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
val author = getOrCreateUser(event.pubKey.toHexKey())
val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.boostedPost.mapNotNull { checkGetOrCreateNote(it) }
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, repliesTo)
@ -409,8 +435,9 @@ object LocalCache {
if (note.event != null) return
val author = getOrCreateUser(event.pubKey.toHexKey())
val mentions = event.originalAuthor.mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.originalPost.mapNotNull { checkGetOrCreateNote(it) }
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, repliesTo)
@ -459,8 +486,9 @@ object LocalCache {
// Already processed this event.
if (note.event != null) return
val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it.key) }
val repliesTo = event.reportedPost.mapNotNull { checkGetOrCreateNote(it.key) }
val mentions = event.reportedAuthor().mapNotNull { checkGetOrCreateUser(it.key) }
val repliesTo = event.reportedPost().mapNotNull { checkGetOrCreateNote(it.key) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, repliesTo)
@ -483,7 +511,7 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey.toHexKey())
if (event.createdAt > oldChannel.updatedMetadataAt) {
if (oldChannel.creator == null || oldChannel.creator == author) {
oldChannel.updateChannelInfo(author, event.channelInfo, event.createdAt)
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
val note = getOrCreateNote(event.id.toHex())
oldChannel.addNote(note)
@ -496,15 +524,16 @@ object LocalCache {
}
}
fun consume(event: ChannelMetadataEvent) {
val channelId = event.channel()
//Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}")
if (event.channel.isNullOrBlank()) return
if (channelId.isNullOrBlank()) return
// new event
val oldChannel = checkGetOrCreateChannel(event.channel) ?: return
val oldChannel = checkGetOrCreateChannel(channelId) ?: return
val author = getOrCreateUser(event.pubKey.toHexKey())
if (event.createdAt > oldChannel.updatedMetadataAt) {
if (oldChannel.creator == null || oldChannel.creator == author) {
oldChannel.updateChannelInfo(author, event.channelInfo, event.createdAt)
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
val note = getOrCreateNote(event.id.toHex())
oldChannel.addNote(note)
@ -518,7 +547,9 @@ object LocalCache {
}
fun consume(event: ChannelMessageEvent, relay: Relay?) {
if (event.channel.isNullOrBlank()) return
val channelId = event.channel()
if (channelId.isNullOrBlank()) return
if (antiSpam.isSpam(event)) {
relay?.let {
it.spamCounter++
@ -526,7 +557,7 @@ object LocalCache {
return
}
val channel = checkGetOrCreateChannel(event.channel) ?: return
val channel = checkGetOrCreateChannel(channelId) ?: return
val note = getOrCreateNote(event.id.toHex())
channel.addNote(note)
@ -541,8 +572,8 @@ object LocalCache {
// Already processed this event.
if (note.event != null) return
val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
val replyTo = event.replyTos
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
val replyTo = event.replyTos()
.mapNotNull { checkGetOrCreateNote(it) }
.filter { it.event !is ChannelCreateEvent }
@ -580,13 +611,16 @@ object LocalCache {
// Already processed this event.
if (note.event != null) return
val zapRequest = event.containedPost()?.id?.toHexKey()?.let { getOrCreateNote(it) }
val author = getOrCreateUser(event.pubKey.toHexKey())
val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost.mapNotNull { checkGetOrCreateNote(it) }
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
((zapRequest?.event as? LnZapRequestEvent)?.taggedAddresses()?.map { getOrCreateAddressableNote(it) } ?: emptySet<Note>())
note.loadEvent(event, author, mentions, repliesTo)
val zapRequest = event.containedPost?.id?.toHexKey()?.let { getOrCreateNote(it) }
if (zapRequest == null) {
Log.e("ZP","Zap Request not found. Unable to process Zap {${event.toJson()}}")
return
@ -617,8 +651,9 @@ object LocalCache {
if (note.event != null) return
val author = getOrCreateUser(event.pubKey.toHexKey())
val mentions = event.zappedAuthor.mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost.mapNotNull { checkGetOrCreateNote(it) }
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, repliesTo)
@ -652,9 +687,13 @@ object LocalCache {
return notes.values.filter {
(it.event is TextNoteEvent && it.event?.content?.contains(text, true) ?: false)
|| (it.event is ChannelMessageEvent && it.event?.content?.contains(text, true) ?: false)
|| (it.event is LongTextNoteEvent && it.event?.content?.contains(text, true) ?: false)
|| it.idHex.startsWith(text, true)
|| it.idNote().startsWith(text, true)
} + addressables.values.filter {
(it.event as? LongTextNoteEvent)?.content?.contains(text, true) ?: false
|| (it.event as? LongTextNoteEvent)?.title()?.contains(text, true) ?: false
|| (it.event as? LongTextNoteEvent)?.summary()?.contains(text, true) ?: false
|| it.idHex.startsWith(text, true)
}
}
@ -738,7 +777,7 @@ object LocalCache {
fun pruneHiddenMessages(account: Account) {
val toBeRemoved = account.hiddenUsers.map {
(users[it]?.notes ?: emptySet()) + (users[it]?.longFormNotes?.values?.flatten() ?: emptySet())
(users[it]?.notes ?: emptySet())
}.flatten()
account.hiddenUsers.forEach {
@ -747,7 +786,6 @@ object LocalCache {
toBeRemoved.forEach {
it.author?.removeNote(it)
it.author?.removeLongFormNote(it)
// reverts the add
it.mentions?.forEach { user ->

Wyświetl plik

@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.model.ATag
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
@ -27,11 +28,18 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nostr.postr.events.Event
import nostr.postr.toHex
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
class Note(val idHex: String) {
class AddressableNote(val address: ATag): Note(address.toNAddr()) {
override fun idNote() = address.toNAddr()
override fun idDisplayNote() = idNote().toShortenHex()
override fun address() = address
override fun createdAt() = (event as? LongTextNoteEvent)?.publishedAt() ?: event?.createdAt
}
open class Note(val idHex: String) {
// These fields are only available after the Text Note event is received.
// They are immutable after that.
var event: Event? = null
@ -57,18 +65,21 @@ class Note(val idHex: String) {
var lastReactionsDownloadTime: Long? = null
fun id() = Hex.decode(idHex)
fun idNote() = id().toNote()
fun idDisplayNote() = idNote().toShortenHex()
open fun idNote() = id().toNote()
open fun idDisplayNote() = idNote().toShortenHex()
fun channel(): Channel? {
val channelHex = (event as? ChannelMessageEvent)?.channel ?:
(event as? ChannelMetadataEvent)?.channel ?:
(event as? ChannelCreateEvent)?.let { idHex }
val channelHex =
(event as? ChannelMessageEvent)?.channel() ?:
(event as? ChannelMetadataEvent)?.channel() ?:
(event as? ChannelCreateEvent)?.let { it.id.toHexKey() }
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
}
fun address() = (event as? LongTextNoteEvent)?.address
open fun address() = (event as? LongTextNoteEvent)?.address()
open fun createdAt() = event?.createdAt
fun loadEvent(event: Event, author: User, mentions: List<User>, replyTo: List<Note>) {
this.event = event
@ -90,14 +101,14 @@ class Note(val idHex: String) {
fun replyLevelSignature(cachedSignatures: MutableMap<Note, String> = mutableMapOf()): String {
val replyTo = replyTo
if (replyTo == null || replyTo.isEmpty()) {
return "/" + formattedDateTime(event?.createdAt ?: 0) + ";"
return "/" + formattedDateTime(createdAt() ?: 0) + ";"
}
return replyTo
.map {
cachedSignatures[it] ?: it.replyLevelSignature(cachedSignatures).apply { cachedSignatures.put(it, this) }
}
.maxBy { it.length }.removeSuffix(";") + "/" + formattedDateTime(event?.createdAt ?: 0) + ";"
.maxBy { it.length }.removeSuffix(";") + "/" + formattedDateTime(createdAt() ?: 0) + ";"
}
fun replyLevel(cachedLevels: MutableMap<Note, Int> = mutableMapOf()): Int {
@ -236,7 +247,7 @@ class Note(val idHex: String) {
val dayAgo = Date().time / 1000 - 24*60*60
return reports.isNotEmpty() ||
(author?.reports?.values?.filter {
it.firstOrNull { ( it.event?.createdAt ?: 0 ) > dayAgo } != null
it.firstOrNull { ( it.createdAt() ?: 0 ) > dayAgo } != null
}?.isNotEmpty() ?: false)
}
@ -283,7 +294,7 @@ class Note(val idHex: String) {
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
val currentTime = Date().time / 1000
return boosts.firstOrNull { it.author == loggedIn && (it.event?.createdAt ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection
return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5)} != null // 5 minute protection
}
fun boostedBy(loggedIn: User): List<Note> {
@ -356,12 +367,21 @@ class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) {
override fun onActive() {
super.onActive()
NostrSingleEventDataSource.add(note.idHex)
if (note is AddressableNote) {
NostrSingleEventDataSource.addAddress(note)
} else {
NostrSingleEventDataSource.add(note)
}
}
override fun onInactive() {
super.onInactive()
NostrSingleEventDataSource.remove(note.idHex)
if (note is AddressableNote) {
NostrSingleEventDataSource.removeAddress(note)
} else {
NostrSingleEventDataSource.remove(note)
}
}
}

Wyświetl plik

@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.model
import com.vitorpamplona.amethyst.service.model.ATag
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@ -34,7 +35,16 @@ class ThreadAssembler {
@OptIn(ExperimentalTime::class)
fun findThreadFor(noteId: String): Set<Note> {
val (result, elapsed) = measureTimedValue {
val note = LocalCache.getOrCreateNote(noteId)
val note = if (noteId.startsWith("naddr")) {
val aTag = ATag.parse(noteId)
if (aTag != null)
LocalCache.getOrCreateAddressableNote(aTag)
else
return emptySet()
} else {
LocalCache.getOrCreateNote(noteId)
}
if (note.event != null) {
val thread = mutableSetOf<Note>()

Wyświetl plik

@ -3,7 +3,6 @@ package com.vitorpamplona.amethyst.model
import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.NostrSingleUserDataSource
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.note.toShortenHex
@ -38,8 +37,7 @@ class User(val pubkeyHex: String) {
var notes = setOf<Note>()
private set
var longFormNotes = mapOf<String, Set<Note>>()
private set
var taggedPosts = setOf<Note>()
private set
@ -145,27 +143,8 @@ class User(val pubkeyHex: String) {
notes = notes - note
}
fun addLongFormNote(note: Note) {
val address = (note.event as LongTextNoteEvent).address
if (address in longFormNotes.keys) {
if (longFormNotes[address]?.contains(note) == false)
longFormNotes = longFormNotes + Pair(address, (longFormNotes[address] ?: emptySet()) + note)
} else {
longFormNotes = longFormNotes + Pair(address, setOf(note))
// No need for Listener yet
}
}
fun removeLongFormNote(note: Note) {
val address = (note.event as LongTextNoteEvent).address ?: return
longFormNotes = longFormNotes - address
}
fun clearNotes() {
notes = setOf<Note>()
longFormNotes = mapOf<String, Set<Note>>()
}
fun addReport(note: Note) {
@ -179,7 +158,7 @@ class User(val pubkeyHex: String) {
liveSet?.reports?.invalidateData()
}
val reportTime = note.event?.createdAt ?: 0
val reportTime = note.createdAt() ?: 0
if (reportTime > latestReportTime) {
latestReportTime = reportTime
}
@ -311,7 +290,7 @@ class User(val pubkeyHex: String) {
fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean {
return reports[loggedIn]?.firstOrNull() {
it.event is ReportEvent && (it.event as ReportEvent).reportedAuthor.any { it.reportType == type }
it.event is ReportEvent && (it.event as ReportEvent).reportedAuthor().any { it.reportType == type }
} != null
}
@ -364,6 +343,7 @@ data class RelayInfo (
data class Chatroom(var roomMessages: Set<Note>)
class UserMetadata {
var name: String? = null
var username: String? = null

Wyświetl plik

@ -1,12 +1,18 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.model.ATag
import java.nio.ByteBuffer
import java.nio.ByteOrder
import nostr.postr.Bech32
import nostr.postr.bechToBytes
import nostr.postr.toByteArray
class Nip19 {
enum class Type {
USER, NOTE
USER, NOTE, RELAY, ADDRESS
}
data class Return(val type: Type, val hex: String)
@ -24,16 +30,31 @@ class Nip19 {
}
if (key.startsWith("nprofile")) {
val tlv = parseTLV(bytes)
val hex = tlv.get(0)?.get(0)?.toHexKey()
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey()
if (hex != null)
return Return(Type.USER, hex)
}
if (key.startsWith("nevent")) {
val tlv = parseTLV(bytes)
val hex = tlv.get(0)?.get(0)?.toHexKey()
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey()
if (hex != null)
return Return(Type.USER, hex)
}
if (key.startsWith("nrelay")) {
val tlv = parseTLV(bytes)
val relayUrl = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8)
if (relayUrl != null)
return Return(Type.RELAY, relayUrl)
}
if (key.startsWith("naddr")) {
val tlv = parseTLV(bytes)
val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8)
val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8)
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey()
val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) }
if (d != null)
return Return(Type.ADDRESS, "$kind:$author:$d")
}
}
} catch (e: Throwable) {
println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
@ -42,22 +63,34 @@ class Nip19 {
return null
}
}
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
var result = mutableMapOf<Byte, MutableList<ByteArray>>()
var rest = data
while (rest.isNotEmpty()) {
val t = rest[0]
val l = rest[1]
val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
rest = rest.sliceArray(IntRange(2 + l, rest.size-1))
if (v.size < l) continue
enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin
SPECIAL(0),
RELAY(1),
AUTHOR(2),
KIND(3);
}
if (!result.containsKey(t)) {
result.put(t, mutableListOf())
}
result.get(t)?.add(v)
fun toInt32(bytes: ByteArray): Int {
require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" }
return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int
}
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
var result = mutableMapOf<Byte, MutableList<ByteArray>>()
var rest = data
while (rest.isNotEmpty()) {
val t = rest[0]
val l = rest[1]
val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
rest = rest.sliceArray(IntRange(2 + l, rest.size-1))
if (v.size < l) continue
if (!result.containsKey(t)) {
result.put(t, mutableListOf())
}
return result
result.get(t)?.add(v)
}
}
return result
}

Wyświetl plik

@ -74,7 +74,7 @@ abstract class NostrDataSource(val debugName: String) {
RepostEvent.kind -> {
val repostEvent = RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)
repostEvent.containedPost?.let { onEvent(it, subscriptionId, relay) }
repostEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) }
LocalCache.consume(repostEvent)
}
ReactionEvent.kind -> LocalCache.consume(ReactionEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
@ -83,7 +83,7 @@ abstract class NostrDataSource(val debugName: String) {
LnZapEvent.kind -> {
val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)
zapEvent.containedPost?.let { onEvent(it, subscriptionId, relay) }
zapEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) }
LocalCache.consume(zapEvent)
}
LnZapRequestEvent.kind -> LocalCache.consume(LnZapRequestEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))

Wyświetl plik

@ -1,6 +1,6 @@
package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
@ -17,10 +17,11 @@ import nostr.postr.JsonFilter
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
private var eventsToWatch = setOf<String>()
private var eventsToWatch = setOf<Note>()
private var addressesToWatch = setOf<Note>()
private fun createAddressFilter(): List<TypedFilter>? {
val addressesToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }.filter { it.address() != null }
val addressesToWatch = eventsToWatch.filter { it.address() != null } + addressesToWatch
if (addressesToWatch.isEmpty()) {
return null
@ -31,22 +32,24 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
return addressesToWatch.filter {
val lastTime = it.lastReactionsDownloadTime
lastTime == null || lastTime < (now - 10)
}.map {
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind
),
tags = mapOf("a" to listOf(it.address()!!)),
since = it.lastReactionsDownloadTime
}.mapNotNull {
it.address()?.let { aTag ->
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(
TextNoteEvent.kind, LongTextNoteEvent.kind, ReactionEvent.kind, RepostEvent.kind, ReportEvent.kind, LnZapEvent.kind, LnZapRequestEvent.kind
),
tags = mapOf("a" to listOf(aTag.toTag())),
since = it.lastReactionsDownloadTime
)
)
)
}
}
}
private fun createRepliesAndReactionsFilter(): List<TypedFilter>? {
val reactionsToWatch = eventsToWatch.map { LocalCache.getOrCreateNote(it) }
val reactionsToWatch = eventsToWatch
if (reactionsToWatch.isEmpty()) {
return null
@ -73,11 +76,9 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
fun createLoadEventsIfNotLoadedFilter(): List<TypedFilter>? {
val directEventsToLoad = eventsToWatch
.map { LocalCache.getOrCreateNote(it) }
.filter { it.event == null }
val threadingEventsToLoad = eventsToWatch
.map { LocalCache.getOrCreateNote(it) }
.mapNotNull { it.replyTo }
.flatten()
.filter { it.event == null }
@ -107,7 +108,7 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
val singleEventChannel = requestNewChannel { time ->
eventsToWatch.forEach {
LocalCache.getOrCreateNote(it).lastReactionsDownloadTime = time
it.lastReactionsDownloadTime = time
}
// Many relays operate with limits in the amount of filters.
// As information comes, the filters will be rotated to get more data.
@ -122,13 +123,23 @@ object NostrSingleEventDataSource: NostrDataSource("SingleEventFeed") {
singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses).flatten().ifEmpty { null }
}
fun add(eventId: String) {
fun add(eventId: Note) {
eventsToWatch = eventsToWatch.plus(eventId)
invalidateFilters()
}
fun remove(eventId: String) {
fun remove(eventId: Note) {
eventsToWatch = eventsToWatch.minus(eventId)
invalidateFilters()
}
fun addAddress(aTag: Note) {
addressesToWatch = addressesToWatch.plus(aTag)
invalidateFilters()
}
fun removeAddress(aTag: Note) {
addressesToWatch = addressesToWatch.minus(aTag)
invalidateFilters()
}
}

Wyświetl plik

@ -0,0 +1,72 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.NIP19TLVTypes
import com.vitorpamplona.amethyst.service.parseTLV
import com.vitorpamplona.amethyst.service.toInt32
import fr.acinq.secp256k1.Hex
import nostr.postr.Bech32
import nostr.postr.bechToBytes
import nostr.postr.toByteArray
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
fun toTag() = "$kind:$pubKeyHex:$dTag"
fun toNAddr(): String {
val kind = kind.toByteArray()
val addr = pubKeyHex.toByteArray()
val dTag = dTag.toByteArray(Charsets.UTF_8)
val fullArray =
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag +
byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr +
byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind
return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32)
}
companion object {
fun parse(address: String): ATag? {
return if (address.startsWith("naddr") || address.startsWith("nostr:naddr"))
parseNAddr(address)
else
parseAtag(address)
}
fun parseAtag(atag: String): ATag? {
return try {
val parts = atag.split(":")
Hex.decode(parts[1])
ATag(parts[0].toInt(), parts[1], parts[2])
} catch (t: Throwable) {
Log.w("Address", "Error parsing A Tag: ${atag}: ${t.message}")
null
}
}
fun parseNAddr(naddr: String): ATag? {
try {
val key = naddr.removePrefix("nostr:")
if (key.startsWith("naddr")) {
val tlv = parseTLV(key.bechToBytes())
val d = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) ?: ""
val relay = tlv.get(NIP19TLVTypes.RELAY.id)?.get(0)?.toString(Charsets.UTF_8)
val author = tlv.get(NIP19TLVTypes.AUTHOR.id)?.get(0)?.toHexKey()
val kind = tlv.get(NIP19TLVTypes.KIND.id)?.get(0)?.let { toInt32(it) }
if (kind != null && author != null)
return ATag(kind, author, d)
}
} catch (e: Throwable) {
println("Issue trying to Decode NIP19 ${this}: ${e.message}")
//e.printStackTrace()
}
return null
}
}
}

Wyświetl plik

@ -14,15 +14,11 @@ class ChannelCreateEvent (
content: String,
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val channelInfo: ChannelData
init {
channelInfo = try {
MetadataEvent.gson.fromJson(content, ChannelData::class.java)
} catch (e: Exception) {
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
ChannelData(null, null, null)
}
fun channelInfo() = try {
MetadataEvent.gson.fromJson(content, ChannelData::class.java)
} catch (e: Exception) {
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
ChannelData(null, null, null)
}
companion object {

Wyświetl plik

@ -12,11 +12,7 @@ class ChannelHideMessageEvent (
content: String,
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val eventsToHide: List<String>
init {
eventsToHide = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
}
fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
companion object {
const val kind = 43

Wyświetl plik

@ -12,15 +12,10 @@ class ChannelMessageEvent (
content: String,
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val channel: String?
@Transient val replyTos: List<String>
@Transient val mentions: List<String>
init {
channel = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
replyTos = tags.filter { it.getOrNull(1) != channel }.mapNotNull { it.getOrNull(1) }
mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
}
fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
fun replyTos() = tags.filter { it.getOrNull(1) != channel() }.mapNotNull { it.getOrNull(1) }
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
companion object {
const val kind = 42

Wyświetl plik

@ -14,19 +14,14 @@ class ChannelMetadataEvent (
content: String,
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val channel: String?
@Transient val channelInfo: ChannelCreateEvent.ChannelData
init {
channel = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
channelInfo =
try {
MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
} catch (e: Exception) {
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
ChannelCreateEvent.ChannelData(null, null, null)
}
}
fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
fun channelInfo() =
try {
MetadataEvent.gson.fromJson(content, ChannelCreateEvent.ChannelData::class.java)
} catch (e: Exception) {
Log.e("ChannelMetadataEvent", "Can't parse channel info $content", e)
ChannelCreateEvent.ChannelData(null, null, null)
}
companion object {
const val kind = 41

Wyświetl plik

@ -12,11 +12,9 @@ class ChannelMuteUserEvent (
content: String,
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val usersToMute: List<String>
init {
usersToMute = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
}
fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
companion object {
const val kind = 44

Wyświetl plik

@ -14,31 +14,25 @@ class LnZapEvent (
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val zappedPost: List<String>
@Transient val zappedAuthor: List<String>
@Transient val containedPost: Event?
@Transient val lnInvoice: String?
@Transient val preimage: String?
@Transient val amount: BigDecimal?
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
init {
zappedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
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) }
lnInvoice = tags.filter { it.firstOrNull() == "bolt11" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
amount = lnInvoice?.let { LnInvoiceUtil.getAmountInSats(lnInvoice) }
preimage = tags.filter { it.firstOrNull() == "preimage" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun lnInvoice() = tags.filter { it.firstOrNull() == "bolt11" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun preimage() = tags.filter { it.firstOrNull() == "preimage" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
val description = tags.filter { it.firstOrNull() == "description" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun description() = tags.filter { it.firstOrNull() == "description" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
containedPost = try {
if (description == null)
null
else
fromJson(description, Client.lenient)
} catch (e: Exception) {
null
// Keeps this as a field because it's a heavier function used everywhere.
val amount = lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
fun containedPost() = try {
description()?.let {
fromJson(it, Client.lenient)
}
} catch (e: Exception) {
null
}
companion object {

Wyświetl plik

@ -13,14 +13,9 @@ class LnZapRequestEvent (
content: String,
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val zappedPost: List<String>
@Transient val zappedAuthor: List<String>
init {
zappedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
zappedAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
}
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) }
companion object {
const val kind = 9734
@ -34,7 +29,7 @@ class LnZapRequestEvent (
listOf("relays") + relays
)
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", originalNote.address) )
tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
}
val id = generateId(pubKey, createdAt, kind, tags, content)

Wyświetl plik

@ -13,33 +13,21 @@ class LongTextNoteEvent(
content: String,
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val replyTos: List<String>
@Transient val mentions: List<String>
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
@Transient val title: String?
@Transient val image: String?
@Transient val summary: String?
@Transient val publishedAt: Long?
@Transient val topics: List<String>
@Transient val address: String
@Transient val dTag: String?
fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
fun address() = ATag(kind, pubKey.toHexKey(), dTag())
init {
replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun image() = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
fun summary() = tags.filter { it.firstOrNull() == "summary" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
dTag = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
address = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: "$kind:${pubKey.toHexKey()}:$dTag"
topics = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
title = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
image = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
summary = tags.filter { it.firstOrNull() == "summary" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
publishedAt = try {
tags.filter { it.firstOrNull() == "published_at" }.mapNotNull { it.getOrNull(1) }.firstOrNull()?.toLong()
} catch (_: Exception) {
null
}
fun publishedAt() = try {
tags.filter { it.firstOrNull() == "published_at" }.mapNotNull { it.getOrNull(1) }.firstOrNull()?.toLong()
} catch (_: Exception) {
null
}
companion object {
@ -59,4 +47,4 @@ class LongTextNoteEvent(
return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig)
}
}
}
}

Wyświetl plik

@ -14,13 +14,9 @@ class ReactionEvent (
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val originalPost: List<String>
@Transient val originalAuthor: List<String>
init {
originalPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
originalAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
}
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) }
companion object {
const val kind = 7
@ -38,7 +34,7 @@ class ReactionEvent (
var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex()))
if (originalNote is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", originalNote.address) )
tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
}
val id = generateId(pubKey, createdAt, kind, tags, content)

Wyświetl plik

@ -17,12 +17,8 @@ class ReportEvent (
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val reportedPost: List<ReportedKey>
@Transient val reportedAuthor: List<ReportedKey>
init {
private fun defaultReportType(): ReportType {
// Works with old and new structures for report.
var reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull()
if (reportType == null) {
reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull()
@ -30,26 +26,29 @@ class ReportEvent (
if (reportType == null) {
reportType = ReportType.SPAM
}
reportedPost = tags
.filter { it.firstOrNull() == "e" && it.getOrNull(1) != null }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: reportType
)
}
reportedAuthor = tags
.filter { it.firstOrNull() == "p" && it.getOrNull(1) != null }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: reportType
)
}
return reportType
}
fun reportedPost() = tags
.filter { it.firstOrNull() == "e" && it.getOrNull(1) != null }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType()
)
}
fun reportedAuthor() = tags
.filter { it.firstOrNull() == "p" && it.getOrNull(1) != null }
.map {
ReportedKey(
it[1],
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: defaultReportType()
)
}
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
companion object {
const val kind = 1984
@ -63,7 +62,7 @@ class ReportEvent (
var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
if (reportedPost is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", reportedPost.address) )
tags = tags + listOf( listOf("a", reportedPost.address().toTag()) )
}
val id = generateId(pubKey, createdAt, kind, tags, content)

Wyświetl plik

@ -15,19 +15,15 @@ class RepostEvent (
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val boostedPost: List<String>
@Transient val originalAuthor: List<String>
@Transient val containedPost: Event?
init {
boostedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
originalAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
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) }
containedPost = try {
fromJson(content, Client.lenient)
} catch (e: Exception) {
null
}
fun containedPost() = try {
fromJson(content, Client.lenient)
} catch (e: Exception) {
null
}
companion object {
@ -43,7 +39,7 @@ class RepostEvent (
var tags:List<List<String>> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor))
if (boostedPost is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", boostedPost.address) )
tags = tags + listOf( listOf("a", boostedPost.address().toTag()) )
}
val id = generateId(pubKey, createdAt, kind, tags, content)

Wyświetl plik

@ -12,20 +12,14 @@ class TextNoteEvent(
content: String,
sig: ByteArray
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient val replyTos: List<String>
@Transient val mentions: List<String>
@Transient val longFormAddress: List<String>
init {
longFormAddress = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }
replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
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 { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
companion object {
const val kind = 1
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, addresses: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent {
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, addresses: List<ATag>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey)
val tags = mutableListOf<List<String>>()
replyTos?.forEach {
@ -35,7 +29,7 @@ class TextNoteEvent(
tags.add(listOf("p", it))
}
addresses?.forEach {
tags.add(listOf("a", it))
tags.add(listOf("a", it.toTag()))
}
val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey)

Wyświetl plik

@ -16,6 +16,6 @@ object ChannelFeedFilter: FeedFilter<Note>() {
// returns the last Note of each user.
override fun feed(): List<Note> {
return channel.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList()
return channel.notes?.values?.filter { account.isAcceptable(it) }?.sortedBy { it.createdAt() }?.reversed() ?: emptyList()
}
}

Wyświetl plik

@ -23,6 +23,6 @@ object ChatroomFeedFilter: FeedFilter<Note>() {
val messages = myAccount.userProfile().privateChatrooms[myUser] ?: return emptyList()
return messages.roomMessages.filter { myAccount.isAcceptable(it) }.sortedBy { it.event?.createdAt }.reversed()
return messages.roomMessages.filter { myAccount.isAcceptable(it) }.sortedBy { it.createdAt() }.reversed()
}
}

Wyświetl plik

@ -17,17 +17,17 @@ object ChatroomListKnownFeedFilter: FeedFilter<Note>() {
val privateMessages = messagingWith.mapNotNull {
privateChatrooms[it]?.roomMessages?.sortedBy {
it.event?.createdAt
it.createdAt()
}?.lastOrNull {
it.event != null
}
}
val publicChannels = account.followingChannels().map {
it.notes.values.filter { account.isAcceptable(it) }.sortedBy { it.event?.createdAt }.lastOrNull { it.event != null }
it.notes.values.filter { account.isAcceptable(it) }.sortedBy { it.createdAt() }.lastOrNull { it.event != null }
}
return (privateMessages + publicChannels).filterNotNull().sortedBy { it.event?.createdAt }.reversed()
return (privateMessages + publicChannels).filterNotNull().sortedBy { it.createdAt() }.reversed()
}
}

Wyświetl plik

@ -17,13 +17,13 @@ object ChatroomListNewFeedFilter: FeedFilter<Note>() {
val privateMessages = messagingWith.mapNotNull {
privateChatrooms[it]?.roomMessages?.sortedBy {
it.event?.createdAt
it.createdAt()
}?.lastOrNull {
it.event != null
}
}
return privateMessages.sortedBy { it.event?.createdAt }.reversed()
return privateMessages.sortedBy { it.createdAt() }.reversed()
}
}

Wyświetl plik

@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object GlobalFeedFilter: FeedFilter<Note>() {
@ -11,11 +12,17 @@ object GlobalFeedFilter: FeedFilter<Note>() {
override fun feed() = LocalCache.notes.values
.filter {
(it.event is TextNoteEvent && (it.event as TextNoteEvent).replyTos.isEmpty()) ||
(it.event is ChannelMessageEvent && (it.event as ChannelMessageEvent).replyTos.isEmpty())
(it.event is TextNoteEvent || it.event is LongTextNoteEvent || it.event is ChannelMessageEvent)
&& it.replyTo.isNullOrEmpty()
}
.filter {
// does not show events already in the public chat list
(it.channel() == null || it.channel() !in account.followingChannels())
// does not show people the user already follows
&& (it.author !in account.userProfile().follows)
}
.filter { account.isAcceptable(it) }
.sortedBy { it.event?.createdAt }
.sortedBy { it.createdAt() }
.reversed()
}

Wyświetl plik

@ -20,7 +20,7 @@ object HomeConversationsFeedFilter: FeedFilter<Note>() {
&& it.author?.let { !HomeNewThreadFeedFilter.account.isHidden(it) } ?: true
&& !it.isNewThread()
}
.sortedBy { it.event?.createdAt }
.sortedBy { it.createdAt() }
.reversed()
}
}

Wyświetl plik

@ -13,7 +13,7 @@ object HomeNewThreadFeedFilter: FeedFilter<Note>() {
override fun feed(): List<Note> {
val user = account.userProfile()
return LocalCache.notes.values
val notes = LocalCache.notes.values
.filter {
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent)
&& it.author in user.follows
@ -21,7 +21,18 @@ object HomeNewThreadFeedFilter: FeedFilter<Note>() {
&& it.author?.let { !account.isHidden(it) } ?: true
&& it.isNewThread()
}
.sortedBy { it.event?.createdAt }
val longFormNotes = LocalCache.addressables.values
.filter {
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent)
&& it.author in user.follows
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
&& it.author?.let { !account.isHidden(it) } ?: true
&& it.isNewThread()
}
return (notes + longFormNotes)
.sortedBy { it.createdAt() }
.reversed()
}
}

Wyświetl plik

@ -63,7 +63,7 @@ object NotificationFeedFilter: FeedFilter<Note>() {
)
}
.sortedBy { it.event?.createdAt }
.sortedBy { it.createdAt() }
.reversed()
}
}

Wyświetl plik

@ -17,7 +17,7 @@ object UserProfileConversationsFeedFilter: FeedFilter<Note>() {
override fun feed(): List<Note> {
return user?.notes
?.filter { account?.isAcceptable(it) == true && !it.isNewThread() }
?.sortedBy { it.event?.createdAt }
?.sortedBy { it.createdAt() }
?.reversed()
?: emptyList()
}

Wyświetl plik

@ -15,9 +15,11 @@ object UserProfileNewThreadFeedFilter: FeedFilter<Note>() {
}
override fun feed(): List<Note> {
return user?.notes?.plus(user?.longFormNotes?.values?.flatten() ?: emptySet())
val longFormNotes = LocalCache.addressables.values.filter { it.author == user }
return user?.notes?.plus(longFormNotes)
?.filter { account?.isAcceptable(it) == true && it.isNewThread() }
?.sortedBy { it.event?.createdAt }
?.sortedBy { it.createdAt() }
?.reversed()
?: emptyList()
}

Wyświetl plik

@ -12,6 +12,6 @@ object UserProfileReportsFeedFilter: FeedFilter<Note>() {
}
override fun feed(): List<Note> {
return user?.reports?.values?.flatten()?.sortedBy { it.event?.createdAt }?.reversed() ?: emptyList()
return user?.reports?.values?.flatten()?.sortedBy { it.createdAt() }?.reversed() ?: emptyList()
}
}

Wyświetl plik

@ -107,7 +107,7 @@ private fun homeHasNewItems(account: Account, cache: NotificationCache, context:
HomeNewThreadFeedFilter.account = account
return (HomeNewThreadFeedFilter.feed().firstOrNull { it.event?.createdAt != null }?.event?.createdAt ?: 0) > lastTime
return (HomeNewThreadFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() ?: 0) > lastTime
}
private fun notificationHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean {
@ -115,17 +115,17 @@ private fun notificationHasNewItems(account: Account, cache: NotificationCache,
NotificationFeedFilter.account = account
return (NotificationFeedFilter.feed().firstOrNull { it.event?.createdAt != null }?.event?.createdAt ?: 0) > lastTime
return (NotificationFeedFilter.feed().firstOrNull { it.createdAt() != null }?.createdAt() ?: 0) > lastTime
}
private fun messagesHasNewItems(account: Account, cache: NotificationCache, context: Context): Boolean {
ChatroomListKnownFeedFilter.account = account
val note = ChatroomListKnownFeedFilter.feed().firstOrNull {
it.event?.createdAt != null && it.channel() == null && it.author != account.userProfile()
it.createdAt() != null && it.channel() == null && it.author != account.userProfile()
} ?: return false
val lastTime = cache.load("Room/${note.author?.pubkeyHex}", context)
return (note.event?.createdAt ?: 0) > lastTime
return (note.createdAt() ?: 0) > lastTime
}

Wyświetl plik

@ -83,8 +83,8 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = notificationCache) {
noteEvent?.let {
hasNewMessages = it.createdAt > notificationCache.cache.load("Channel/${channel.idHex}", context)
note.createdAt()?.let {
hasNewMessages = it > notificationCache.cache.load("Channel/${channel.idHex}", context)
}
}
@ -103,7 +103,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
channelLastTime = note.event?.createdAt,
channelLastTime = note.createdAt(),
channelLastContent = "${author?.toBestDisplayName()}: " + description,
hasNewMessages = hasNewMessages,
onClick = { navController.navigate("Channel/${channel.idHex}") })
@ -134,7 +134,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
ChannelName(
channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) },
channelTitle = { UsernameDisplay(userToComposeOn, it) },
channelLastTime = noteEvent?.createdAt,
channelLastTime = note.createdAt(),
channelLastContent = accountViewModel.decrypt(note),
hasNewMessages = hasNewMessages,
onClick = { navController.navigate("Room/${user.pubkeyHex}") })

Wyświetl plik

@ -134,7 +134,7 @@ fun ChatroomMessageCompose(
routeForLastRead?.let {
val lastTime = NotificationCache.load(it, context)
val createdAt = note.event?.createdAt
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
@ -241,16 +241,16 @@ fun ChatroomMessageCompose(
val event = note.event
if (event is ChannelCreateEvent) {
Text(text = note.author?.toBestDisplayName()
.toString() + " ${stringResource(R.string.created)} " + (event.channelInfo.name
?: "") +" ${stringResource(R.string.with_description_of)} '" + (event.channelInfo.about
?: "") + "', ${stringResource(R.string.and_picture)} '" + (event.channelInfo.picture
.toString() + " ${stringResource(R.string.created)} " + (event.channelInfo().name
?: "") +" ${stringResource(R.string.with_description_of)} '" + (event.channelInfo().about
?: "") + "', ${stringResource(R.string.and_picture)} '" + (event.channelInfo().picture
?: "") + "'"
)
} else if (event is ChannelMetadataEvent) {
Text(text = note.author?.toBestDisplayName()
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (event.channelInfo.name
?: "") + "$', {stringResource(R.string.description_to)} '" + (event.channelInfo.about
?: "") + "', ${stringResource(R.string.and_picture_to)} '" + (event.channelInfo.picture
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (event.channelInfo().name
?: "") + "$', {stringResource(R.string.description_to)} '" + (event.channelInfo().about
?: "") + "', ${stringResource(R.string.and_picture_to)} '" + (event.channelInfo().picture
?: "") + "'"
)
} else {
@ -295,7 +295,7 @@ fun ChatroomMessageCompose(
) {
Row() {
Text(
timeAgoShort(note.event?.createdAt, context),
timeAgoShort(note.createdAt(), context),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
fontSize = 12.sp
)

Wyświetl plik

@ -107,7 +107,7 @@ fun NoteCompose(
routeForLastRead?.let {
val lastTime = NotificationCache.load(it, context)
val createdAt = noteEvent.createdAt
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
@ -241,7 +241,7 @@ fun NoteCompose(
}
Text(
timeAgo(noteEvent.createdAt, context = context),
timeAgo(note.createdAt(), context = context),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1
)
@ -322,7 +322,7 @@ fun NoteCompose(
)
}
} else if (noteEvent is ReportEvent) {
val reportType = (noteEvent.reportedPost + noteEvent.reportedAuthor).map {
val reportType = (noteEvent.reportedPost() + noteEvent.reportedAuthor()).map {
when (it.reportType) {
ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content)
ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity)
@ -343,50 +343,7 @@ fun NoteCompose(
thickness = 0.25.dp
)
} else if (noteEvent is LongTextNoteEvent) {
Row(
modifier = Modifier
.clip(shape = RoundedCornerShape(15.dp))
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp))
) {
Column {
noteEvent.image?.let {
AsyncImage(
model = noteEvent.image,
contentDescription = stringResource(
R.string.preview_card_image_for,
noteEvent.image
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
noteEvent.title?.let {
Text(
text = it,
style = MaterialTheme.typography.body2,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
noteEvent.summary?.let {
Text(
text = it,
style = MaterialTheme.typography.caption,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
}
LongFormHeader(noteEvent)
ReactionsRow(note, accountViewModel)
@ -429,6 +386,56 @@ fun NoteCompose(
}
}
@Composable
private fun LongFormHeader(noteEvent: LongTextNoteEvent) {
Row(
modifier = Modifier
.clip(shape = RoundedCornerShape(15.dp))
.border(
1.dp,
MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
RoundedCornerShape(15.dp)
)
) {
Column {
noteEvent.image()?.let {
AsyncImage(
model = it,
contentDescription = stringResource(
R.string.preview_card_image_for,
it
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
noteEvent.title()?.let {
Text(
text = it,
style = MaterialTheme.typography.body1,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
)
}
noteEvent.summary()?.let {
Text(
text = it,
style = MaterialTheme.typography.caption,
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
color = Color.Gray,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
@Composable
private fun RelayBadges(baseNote: Note) {
val noteRelaysState by baseNote.live().relays.observeAsState()

Wyświetl plik

@ -10,14 +10,14 @@ abstract class Card() {
class NoteCard(val note: Note): Card() {
override fun createdAt(): Long {
return note.event?.createdAt ?: 0
return note.createdAt() ?: 0
}
override fun id() = note.idHex
}
class LikeSetCard(val note: Note, val likeEvents: List<Note>): Card() {
val createdAt = likeEvents.maxOf { it.event?.createdAt ?: 0 }
val createdAt = likeEvents.maxOf { it.createdAt() ?: 0 }
override fun createdAt(): Long {
return createdAt
}
@ -25,7 +25,7 @@ class LikeSetCard(val note: Note, val likeEvents: List<Note>): Card() {
}
class ZapSetCard(val note: Note, val zapEvents: Map<Note, Note>): Card() {
val createdAt = zapEvents.maxOf { it.value.event?.createdAt ?: 0 }
val createdAt = zapEvents.maxOf { it.value.createdAt() ?: 0 }
override fun createdAt(): Long {
return createdAt
}
@ -34,9 +34,9 @@ class ZapSetCard(val note: Note, val zapEvents: Map<Note, Note>): Card() {
class MultiSetCard(val note: Note, val boostEvents: List<Note>, val likeEvents: List<Note>, val zapEvents: Map<Note, Note>): Card() {
val createdAt = maxOf(
zapEvents.maxOfOrNull { it.value.event?.createdAt ?: 0 } ?: 0 ,
likeEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0 ,
boostEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0
zapEvents.maxOfOrNull { it.value.createdAt() ?: 0 } ?: 0 ,
likeEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0 ,
boostEvents.maxOfOrNull { it.createdAt() ?: 0 } ?: 0
)
override fun createdAt(): Long {
@ -46,7 +46,7 @@ class MultiSetCard(val note: Note, val boostEvents: List<Note>, val likeEvents:
}
class BoostSetCard(val note: Note, val boostEvents: List<Note>): Card() {
val createdAt = boostEvents.maxOf { it.event?.createdAt ?: 0 }
val createdAt = boostEvents.maxOf { it.createdAt() ?: 0 }
override fun createdAt(): Long {
return createdAt

Wyświetl plik

@ -97,11 +97,6 @@ private fun FeedLoaded(
) {
val listState = rememberLazyListState()
LaunchedEffect(Unit) {
delay(500)
listState.animateScrollToItem(0)
}
LazyColumn(
contentPadding = PaddingValues(
top = 10.dp,

Wyświetl plik

@ -243,7 +243,7 @@ fun NoteMaster(baseNote: Note,
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
Text(
timeAgo(noteEvent.createdAt, context = context),
timeAgo(note.createdAt(), context = context),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1
)
@ -270,19 +270,19 @@ fun NoteMaster(baseNote: Note,
if (noteEvent is LongTextNoteEvent) {
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)) {
Column {
noteEvent.image?.let {
noteEvent.image()?.let {
AsyncImage(
model = noteEvent.image,
model = it,
contentDescription = stringResource(
R.string.preview_card_image_for,
noteEvent.image
it
),
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
noteEvent.title?.let {
noteEvent.title()?.let {
Text(
text = it,
fontSize = 30.sp,
@ -293,7 +293,7 @@ fun NoteMaster(baseNote: Note,
)
}
noteEvent.summary?.let {
noteEvent.summary()?.let {
Text(
text = it,
modifier = Modifier

Wyświetl plik

@ -7,7 +7,7 @@
<string name="scan_qr">Scan QR</string>
<string name="show_anyway">Show Anyway</string>
<string name="post_was_flagged_as_inappropriate_by">Post was flagged as inappropriate by</string>
<string name="post_not_found">post not found</string>
<string name="post_not_found">Post not found</string>
<string name="channel_image">Channel Image</string>
<string name="referenced_event_not_found">Referenced event not found</string>
<string name="could_not_decrypt_the_message">Could Not decrypt the message</string>

Wyświetl plik

@ -0,0 +1,33 @@
package com.vitorpamplona.amethyst
import com.vitorpamplona.amethyst.service.Nip19
import com.vitorpamplona.amethyst.service.model.ATag
import com.vitorpamplona.amethyst.service.toNAddr
import org.junit.Assert.assertEquals
import org.junit.Test
class NIP19ParserTest {
@Test
fun nAddrParser() {
val result = Nip19().uriToRoute("nostr:naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus")
assertEquals("30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:", result?.hex)
}
@Test
fun nAddrParser2() {
val result = Nip19().uriToRoute("nostr:naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8")
assertEquals("30023:d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c:guide-wireguard", result?.hex)
}
@Test
fun nAddrFormatter() {
val address = ATag(30023, "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", "" )
assertEquals("naddr1qqqqygzxpsj7dqha57pjk5k37gkn6g4nzakewtmqmnwryyhd3jfwlpgxtspsgqqqw4rs3xyxus", address.toNAddr())
}
@Test
fun nAddrFormatter2() {
val address = ATag(30023, "d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c", "guide-wireguard" )
assertEquals("naddr1qq8kwatfv3jj6amfwfjkwatpwfjqygxsm6lelvfda7qlg0tud9pfhduysy4vrexj65azqtdk4tr75j6xdspsgqqqw4rsg32ag8", address.toNAddr())
}
}