Migrates Events to the App's source code as opposed to NostrPostr:

- Changes them to remove all secondary fields and turn them into functions
- Changes them to from being based in ByteArrays to String (since we use Hex everywhere and strings are immutable, we avoid duplicating memory with ByteArrays)
This commit is contained in:
Vitor Pamplona
2023-03-03 16:00:47 -05:00
parent da9027b430
commit 5ae552117d
44 changed files with 729 additions and 308 deletions
+1 -1
View File
@@ -25,7 +25,7 @@
-keep class fr.acinq.secp256k1.jni.** { *; } -keep class fr.acinq.secp256k1.jni.** { *; }
# For the NostrPostr library # For the NostrPostr library
-keep class nostr.postr.** { *; } -keep class nostr.postr.** { *; }
-keep class nostr.postr.events.** { *; } -keep class com.vitorpamplona.amethyst.service.model.** { *; }
# Json parsing # Json parsing
-keep class com.google.gson.reflect.** { *; } -keep class com.google.gson.reflect.** { *; }
-keep class * extends com.google.gson.reflect.TypeToken -keep class * extends com.google.gson.reflect.TypeToken
@@ -8,9 +8,9 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo
import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.model.toByteArray
import java.util.Locale import java.util.Locale
import nostr.postr.Persona import nostr.postr.Persona
import nostr.postr.events.ContactListEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent
import nostr.postr.events.Event import com.vitorpamplona.amethyst.service.model.Event
import nostr.postr.events.Event.Companion.getRefinedEvent import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent
import nostr.postr.toHex import nostr.postr.toHex
class LocalPreferences(context: Context) { class LocalPreferences(context: Context) {
@@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.Contact
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
@@ -26,16 +27,12 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nostr.postr.Contact
import nostr.postr.Persona import nostr.postr.Persona
import nostr.postr.Utils import com.vitorpamplona.amethyst.service.model.ContactListEvent
import nostr.postr.events.ContactListEvent import com.vitorpamplona.amethyst.service.model.DeletionEvent
import nostr.postr.events.DeletionEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent
import nostr.postr.events.Event import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import nostr.postr.events.MetadataEvent
import nostr.postr.events.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import nostr.postr.toHex
val DefaultChannels = setOf( val DefaultChannels = setOf(
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr
@@ -89,10 +86,11 @@ class Account(
if (!isWriteable()) return if (!isWriteable()) return
val contactList = userProfile().latestContactList val contactList = userProfile().latestContactList
val follows = contactList?.follows() ?: emptyList()
if (contactList != null && contactList.follows.size > 0) { if (contactList != null && follows.isNotEmpty()) {
val event = ContactListEvent.create( val event = ContactListEvent.create(
contactList.follows, follows,
relays, relays,
loggedIn.privKey!!) loggedIn.privKey!!)
@@ -111,14 +109,7 @@ class Account(
if (!isWriteable()) return if (!isWriteable()) return
loggedIn.privKey?.let { loggedIn.privKey?.let {
val createdAt = Date().time / 1000 val event = MetadataEvent.create(toString, loggedIn.privKey!!)
val content = toString
val pubKey = Utils.pubkeyCreate(it)
val tags = listOf<List<String>>()
val id = Event.generateId(pubKey, createdAt, MetadataEvent.kind, tags, content)
val sig = Utils.sign(id, it)
val event = MetadataEvent(id, pubKey, createdAt, tags, content, sig)
Client.send(event) Client.send(event)
LocalCache.consume(event) LocalCache.consume(event)
} }
@@ -250,10 +241,11 @@ class Account(
if (!isWriteable()) return if (!isWriteable()) return
val contactList = userProfile().latestContactList val contactList = userProfile().latestContactList
val follows = contactList?.follows() ?: emptyList()
val event = if (contactList != null && contactList.follows.size > 0) { val event = if (contactList != null && follows.isNotEmpty()) {
ContactListEvent.create( ContactListEvent.create(
contactList.follows.plus(Contact(user.pubkeyHex, null)), follows.plus(Contact(user.pubkeyHex, null)),
userProfile().relays, userProfile().relays,
loggedIn.privKey!!) loggedIn.privKey!!)
} else { } else {
@@ -273,10 +265,11 @@ class Account(
if (!isWriteable()) return if (!isWriteable()) return
val contactList = userProfile().latestContactList val contactList = userProfile().latestContactList
val follows = contactList?.follows() ?: emptyList()
if (contactList != null && contactList.follows.size > 0) { if (contactList != null && follows.isNotEmpty()) {
val event = ContactListEvent.create( val event = ContactListEvent.create(
contactList.follows.filter { it.pubKeyHex != user.pubkeyHex }, follows.filter { it.pubKeyHex != user.pubkeyHex },
userProfile().relays, userProfile().relays,
loggedIn.privKey!!) loggedIn.privKey!!)
@@ -320,37 +313,6 @@ class Account(
LocalCache.consume(signedEvent, null) LocalCache.consume(signedEvent, null)
} }
fun createPrivateMessageWithReply(
recipientPubKey: ByteArray,
msg: String,
replyTos: List<String>? = null, mentions: List<String>? = null,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000,
publishedRecipientPubKey: ByteArray? = null,
advertiseNip18: Boolean = true
): PrivateDmEvent {
val content = Utils.encrypt(
if (advertiseNip18) {
PrivateDmEvent.nip18Advertisement
} else { "" } + msg,
privateKey,
recipientPubKey)
val pubKey = Utils.pubkeyCreate(privateKey)
val tags = mutableListOf<List<String>>()
publishedRecipientPubKey?.let {
tags.add(listOf("p", publishedRecipientPubKey.toHex()))
}
replyTos?.forEach {
tags.add(listOf("e", it))
}
mentions?.forEach {
tags.add(listOf("p", it))
}
val id = Event.generateId(pubKey, createdAt, PrivateDmEvent.kind, tags, content)
val sig = Utils.sign(id, privateKey)
return PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
}
fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) { fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) {
if (!isWriteable()) return if (!isWriteable()) return
val user = LocalCache.users[toUser] ?: return val user = LocalCache.users[toUser] ?: return
@@ -358,7 +320,7 @@ class Account(
val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null }
val mentionsHex = emptyList<String>() val mentionsHex = emptyList<String>()
val signedEvent = createPrivateMessageWithReply( val signedEvent = PrivateDmEvent.create(
recipientPubKey = user.pubkey(), recipientPubKey = user.pubkey(),
publishedRecipientPubKey = user.pubkey(), publishedRecipientPubKey = user.pubkey(),
msg = message, msg = message,
@@ -386,7 +348,7 @@ class Account(
Client.send(event) Client.send(event)
LocalCache.consume(event) LocalCache.consume(event)
joinChannel(event.id.toHex()) joinChannel(event.id)
} }
fun joinChannel(idHex: String) { fun joinChannel(idHex: String) {
@@ -438,7 +400,7 @@ class Account(
Client.send(event) Client.send(event)
LocalCache.consume(event) LocalCache.consume(event)
joinChannel(event.id.toHex()) joinChannel(event.id)
} }
fun decryptContent(note: Note): String? { fun decryptContent(note: Note): String? {
@@ -446,26 +408,12 @@ class Account(
return if (event is PrivateDmEvent && loggedIn.privKey != null) { return if (event is PrivateDmEvent && loggedIn.privKey != null) {
var pubkeyToUse = event.pubKey var pubkeyToUse = event.pubKey
val recepientPK = event.recipientPubKey val recepientPK = event.recipientPubKey()
if (note.author == userProfile() && recepientPK != null) if (note.author == userProfile() && recepientPK != null)
pubkeyToUse = recepientPK pubkeyToUse = recepientPK
return try { event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray())
val sharedSecret = Utils.getSharedSecret(loggedIn.privKey!!, pubkeyToUse)
val retVal = Utils.decrypt(event.content, sharedSecret)
if (retVal.startsWith(PrivateDmEvent.nip18Advertisement)) {
retVal.substring(16)
} else {
retVal
}
} catch (e: Exception) {
e.printStackTrace()
null
}
} else { } else {
event?.content event?.content
} }
@@ -495,10 +443,10 @@ class Account(
} }
private fun updateContactListTo(newContactList: ContactListEvent?) { private fun updateContactListTo(newContactList: ContactListEvent?) {
if (newContactList?.follows.isNullOrEmpty()) return if (newContactList?.follows().isNullOrEmpty()) return
// Events might be different objects, we have to compare their ids. // Events might be different objects, we have to compare their ids.
if (backupContactList?.id?.toHex() != newContactList?.id?.toHex()) { if (backupContactList?.id != newContactList?.id) {
backupContactList = newContactList backupContactList = newContactList
saveable.invalidateData() saveable.invalidateData()
} }
@@ -11,7 +11,7 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nostr.postr.events.Event import com.vitorpamplona.amethyst.service.model.Event
import nostr.postr.toHex import nostr.postr.toHex
data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Set<HexKey>) data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Set<HexKey>)
@@ -22,7 +22,7 @@ class AntiSpamFilter {
@Synchronized @Synchronized
fun isSpam(event: Event): Boolean { fun isSpam(event: Event): Boolean {
val idHex = event.id.toHexKey() val idHex = event.id
// if already processed, ok // if already processed, ok
if (LocalCache.notes[idHex] != null) return false if (LocalCache.notes[idHex] != null) return false
@@ -37,15 +37,15 @@ class AntiSpamFilter {
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode() val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) { if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) {
Log.w("Potential SPAM Message", "${event.id.toHex()} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}") Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}")
// Log down offenders // Log down offenders
if (spamMessages.get(hash) == null) { if (spamMessages.get(hash) == null) {
spamMessages.put(hash, Spammer(event.pubKey.toHexKey(), setOf(recentMessages[hash], event.id.toHex()))) spamMessages.put(hash, Spammer(event.pubKey, setOf(recentMessages[hash], event.id)))
liveSpam.invalidateData() liveSpam.invalidateData()
} else { } else {
val spammer = spamMessages.get(hash) val spammer = spamMessages.get(hash)
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id.toHex() spammer.duplicatedMessages = spammer.duplicatedMessages + event.id
liveSpam.invalidateData() liveSpam.invalidateData()
} }
@@ -17,6 +17,13 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.DeletionEvent
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@@ -32,13 +39,6 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nostr.postr.events.ContactListEvent
import nostr.postr.events.DeletionEvent
import nostr.postr.events.Event
import nostr.postr.events.MetadataEvent
import nostr.postr.events.PrivateDmEvent
import nostr.postr.events.RecommendRelayEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import nostr.postr.toHex import nostr.postr.toHex
import nostr.postr.toNpub import nostr.postr.toNpub
@@ -139,7 +139,7 @@ object LocalCache {
fun consume(event: MetadataEvent) { fun consume(event: MetadataEvent) {
// new event // new event
val oldUser = getOrCreateUser(event.pubKey.toHexKey()) val oldUser = getOrCreateUser(event.pubKey)
if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) { if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) {
val newUser = try { val newUser = try {
metadataParser.readValue( metadataParser.readValue(
@@ -173,8 +173,8 @@ object LocalCache {
return return
} }
val note = getOrCreateNote(event.id.toHex()) val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
if (relay != null) { if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt) author.addRelayBeingUsed(relay, event.createdAt)
@@ -220,7 +220,7 @@ object LocalCache {
} }
val note = getOrCreateAddressableNote(event.address()) val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
if (relay != null) { if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt) author.addRelayBeingUsed(relay, event.createdAt)
@@ -228,7 +228,7 @@ object LocalCache {
} }
// Already processed this event. // Already processed this event.
if (note.event?.id?.toHex() == event.id.toHex()) return if (note.event?.id == event.id) return
val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.mentions().mapNotNull { checkGetOrCreateUser(it) }
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
@@ -284,14 +284,15 @@ object LocalCache {
} }
fun consume(event: ContactListEvent) { fun consume(event: ContactListEvent) {
val user = getOrCreateUser(event.pubKey.toHexKey()) val user = getOrCreateUser(event.pubKey)
val follows = event.follows()
if (event.createdAt > user.updatedFollowsAt && event.follows.isNotEmpty()) { if (event.createdAt > user.updatedFollowsAt && !follows.isNullOrEmpty()) {
// Saves relay list only if it's a user that is currently been seen // Saves relay list only if it's a user that is currently been seen
user.latestContactList = event user.latestContactList = event
user.updateFollows( user.updateFollows(
event.follows.map { follows.map {
try { try {
val pubKey = decodePublicKey(it.pubKeyHex) val pubKey = decodePublicKey(it.pubKeyHex)
getOrCreateUser(pubKey.toHexKey()) getOrCreateUser(pubKey.toHexKey())
@@ -316,20 +317,17 @@ object LocalCache {
user.updateRelays(relays) user.updateRelays(relays)
} }
} catch (e: Exception) { } catch (e: Exception) {
println("relay import issue") Log.w("Relay List Parser","Relay import issue ${e.message}", e)
e.printStackTrace() e.printStackTrace()
} }
Log.d( Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
"CL",
"AAA ${user.toBestDisplayName()} ${event.follows.size}"
)
} }
} }
fun consume(event: PrivateDmEvent, relay: Relay?) { fun consume(event: PrivateDmEvent, relay: Relay?) {
val note = getOrCreateNote(event.id.toHex()) val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
if (relay != null) { if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt) author.addRelayBeingUsed(relay, event.createdAt)
@@ -339,7 +337,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
val recipient = event.recipientPubKey?.let { getOrCreateUser(it.toHexKey()) } val recipient = event.recipientPubKey()?.let { getOrCreateUser(it) }
//Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}") //Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
@@ -359,9 +357,9 @@ object LocalCache {
fun consume(event: DeletionEvent) { fun consume(event: DeletionEvent) {
var deletedAtLeastOne = false var deletedAtLeastOne = false
event.deleteEvents.mapNotNull { notes[it] }.forEach { deleteNote -> event.deleteEvents().mapNotNull { notes[it] }.forEach { deleteNote ->
// must be the same author // must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey.toHexKey()) { if (deleteNote.author?.pubkeyHex == event.pubKey) {
deleteNote.author?.removeNote(deleteNote) deleteNote.author?.removeNote(deleteNote)
// reverts the add // reverts the add
@@ -395,14 +393,14 @@ object LocalCache {
} }
fun consume(event: RepostEvent) { fun consume(event: RepostEvent) {
val note = getOrCreateNote(event.id.toHex()) val note = getOrCreateNote(event.id)
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
//Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}") //Log.d("TN", "New Boost (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${formattedDateTime(event.createdAt)}")
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
@@ -429,12 +427,12 @@ object LocalCache {
} }
fun consume(event: ReactionEvent) { fun consume(event: ReactionEvent) {
val note = getOrCreateNote(event.id.toHexKey()) val note = getOrCreateNote(event.id)
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
@@ -475,8 +473,8 @@ object LocalCache {
} }
fun consume(event: ReportEvent, relay: Relay?) { fun consume(event: ReportEvent, relay: Relay?) {
val note = getOrCreateNote(event.id.toHex()) val note = getOrCreateNote(event.id)
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
if (relay != null) { if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt) author.addRelayBeingUsed(relay, event.createdAt)
@@ -507,13 +505,13 @@ object LocalCache {
fun consume(event: ChannelCreateEvent) { fun consume(event: ChannelCreateEvent) {
//Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") //Log.d("MT", "New Event ${event.content} ${event.id.toHex()}")
// new event // new event
val oldChannel = getOrCreateChannel(event.id.toHex()) val oldChannel = getOrCreateChannel(event.id)
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
if (event.createdAt > oldChannel.updatedMetadataAt) { if (event.createdAt > oldChannel.updatedMetadataAt) {
if (oldChannel.creator == null || oldChannel.creator == author) { 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()) val note = getOrCreateNote(event.id)
oldChannel.addNote(note) oldChannel.addNote(note)
note.loadEvent(event, author, emptyList(), emptyList()) note.loadEvent(event, author, emptyList(), emptyList())
@@ -530,12 +528,12 @@ object LocalCache {
// new event // new event
val oldChannel = checkGetOrCreateChannel(channelId) ?: return val oldChannel = checkGetOrCreateChannel(channelId) ?: return
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
if (event.createdAt > oldChannel.updatedMetadataAt) { if (event.createdAt > oldChannel.updatedMetadataAt) {
if (oldChannel.creator == null || oldChannel.creator == author) { 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()) val note = getOrCreateNote(event.id)
oldChannel.addNote(note) oldChannel.addNote(note)
note.loadEvent(event, author, emptyList(), emptyList()) note.loadEvent(event, author, emptyList(), emptyList())
@@ -559,10 +557,10 @@ object LocalCache {
val channel = checkGetOrCreateChannel(channelId) ?: return val channel = checkGetOrCreateChannel(channelId) ?: return
val note = getOrCreateNote(event.id.toHex()) val note = getOrCreateNote(event.id)
channel.addNote(note) channel.addNote(note)
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
if (relay != null) { if (relay != null) {
author.addRelayBeingUsed(relay, event.createdAt) author.addRelayBeingUsed(relay, event.createdAt)
@@ -606,14 +604,14 @@ object LocalCache {
} }
fun consume(event: LnZapEvent) { fun consume(event: LnZapEvent) {
val note = getOrCreateNote(event.id.toHexKey()) val note = getOrCreateNote(event.id)
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
val zapRequest = event.containedPost()?.id?.toHexKey()?.let { getOrCreateNote(it) } val zapRequest = event.containedPost()?.id?.let { getOrCreateNote(it) }
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
@@ -645,15 +643,15 @@ object LocalCache {
} }
fun consume(event: LnZapRequestEvent) { fun consume(event: LnZapRequestEvent) {
val note = getOrCreateNote(event.id.toHexKey()) val note = getOrCreateNote(event.id)
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
val author = getOrCreateUser(event.pubKey.toHexKey()) val author = getOrCreateUser(event.pubKey)
val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) }
val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } event.taggedAddresses().map { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, mentions, repliesTo) note.loadEvent(event, author, mentions, repliesTo)
@@ -27,7 +27,7 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nostr.postr.events.Event import com.vitorpamplona.amethyst.service.model.Event
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
@@ -72,7 +72,7 @@ open class Note(val idHex: String) {
val channelHex = val channelHex =
(event as? ChannelMessageEvent)?.channel() ?: (event as? ChannelMessageEvent)?.channel() ?:
(event as? ChannelMetadataEvent)?.channel() ?: (event as? ChannelMetadataEvent)?.channel() ?:
(event as? ChannelCreateEvent)?.let { it.id.toHexKey() } (event as? ChannelCreateEvent)?.let { it.id }
return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) }
} }
@@ -251,6 +251,22 @@ open class Note(val idHex: String) {
}?.isNotEmpty() ?: false) }?.isNotEmpty() ?: false)
} }
fun directlyCiteUsersHex(): Set<HexKey> {
val matcher = tagSearch.matcher(event?.content ?: "")
val returningList = mutableSetOf<String>()
while (matcher.find()) {
try {
val tag = matcher.group(1)?.let { event?.tags?.get(it.toInt()) }
if (tag != null && tag[0] == "p") {
returningList.add(tag[1])
}
} catch (e: Exception) {
}
}
return returningList
}
fun directlyCiteUsers(): Set<User> { fun directlyCiteUsers(): Set<User> {
val matcher = tagSearch.matcher(event?.content ?: "") val matcher = tagSearch.matcher(event?.content ?: "")
val returningList = mutableSetOf<User>() val returningList = mutableSetOf<User>()
@@ -18,8 +18,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nostr.postr.Bech32 import nostr.postr.Bech32
import nostr.postr.events.ContactListEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent
import nostr.postr.events.MetadataEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent
import nostr.postr.toNpub import nostr.postr.toNpub
val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)") val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)")
@@ -9,8 +9,8 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent
import nostr.postr.events.MetadataEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrAccountDataSource: NostrDataSource("AccountData") { object NostrAccountDataSource: NostrDataSource("AccountData") {
@@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") { object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") {
lateinit var account: Account lateinit var account: Account
@@ -7,7 +7,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") {
lateinit var account: Account lateinit var account: Account
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import android.util.Log
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
@@ -15,7 +16,6 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.service.relays.Subscription import com.vitorpamplona.amethyst.service.relays.Subscription
import com.vitorpamplona.amethyst.service.relays.hasValidSignature
import java.util.Date import java.util.Date
import java.util.UUID import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -26,12 +26,12 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nostr.postr.events.ContactListEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent
import nostr.postr.events.DeletionEvent import com.vitorpamplona.amethyst.service.model.DeletionEvent
import nostr.postr.events.Event import com.vitorpamplona.amethyst.service.model.Event
import nostr.postr.events.MetadataEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent
import nostr.postr.events.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import nostr.postr.events.RecommendRelayEvent import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
abstract class NostrDataSource(val debugName: String) { abstract class NostrDataSource(val debugName: String) {
@@ -62,39 +62,32 @@ abstract class NostrDataSource(val debugName: String) {
try { try {
when (event) { when (event) {
is MetadataEvent -> LocalCache.consume(event) is ChannelCreateEvent -> LocalCache.consume(event)
//is TextNoteEvent -> LocalCache.consume(event, relay) overrides default TextNote is ChannelHideMessageEvent -> LocalCache.consume(event)
is RecommendRelayEvent -> LocalCache.consume(event) is ChannelMessageEvent -> LocalCache.consume(event, relay)
is ChannelMetadataEvent -> LocalCache.consume(event)
is ChannelMuteUserEvent -> LocalCache.consume(event)
is ContactListEvent -> LocalCache.consume(event) is ContactListEvent -> LocalCache.consume(event)
is PrivateDmEvent -> LocalCache.consume(event, relay)
is DeletionEvent -> LocalCache.consume(event) is DeletionEvent -> LocalCache.consume(event)
else -> when (event.kind) {
TextNoteEvent.kind -> LocalCache.consume(TextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
RepostEvent.kind -> { is LnZapEvent -> {
val repostEvent = RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) event.containedPost()?.let { onEvent(it, subscriptionId, relay) }
LocalCache.consume(event)
repostEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) } }
LocalCache.consume(repostEvent) is LnZapRequestEvent -> LocalCache.consume(event)
} is LongTextNoteEvent -> LocalCache.consume(event, relay)
ReactionEvent.kind -> LocalCache.consume(ReactionEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) is MetadataEvent -> LocalCache.consume(event)
ReportEvent.kind -> LocalCache.consume(ReportEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) is PrivateDmEvent -> LocalCache.consume(event, relay)
is ReactionEvent -> LocalCache.consume(event)
LnZapEvent.kind -> { is RecommendRelayEvent -> LocalCache.consume(event)
val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) is ReportEvent -> LocalCache.consume(event, relay)
is RepostEvent -> {
zapEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) } event.containedPost()?.let { onEvent(it, subscriptionId, relay) }
LocalCache.consume(zapEvent) LocalCache.consume(event)
} }
LnZapRequestEvent.kind -> LocalCache.consume(LnZapRequestEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) is TextNoteEvent -> LocalCache.consume(event, relay)
else -> {
ChannelCreateEvent.kind -> LocalCache.consume(ChannelCreateEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) Log.w("Event Not Supported", event.toJson())
ChannelMetadataEvent.kind -> LocalCache.consume(ChannelMetadataEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
ChannelMessageEvent.kind -> LocalCache.consume(ChannelMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
ChannelHideMessageEvent.kind -> LocalCache.consume(ChannelHideMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
ChannelMuteUserEvent.kind -> LocalCache.consume(ChannelMuteUserEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
LongTextNoteEvent.kind -> LocalCache.consume(LongTextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.bechToBytes import nostr.postr.bechToBytes
import nostr.postr.events.MetadataEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent
import nostr.postr.toHex import nostr.postr.toHex
object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") { object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") {
@@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.MetadataEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent
object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") {
var usersToWatch = setOf<User>() var usersToWatch = setOf<User>()
@@ -7,8 +7,8 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.FeedType
import com.vitorpamplona.amethyst.service.relays.TypedFilter import com.vitorpamplona.amethyst.service.relays.TypedFilter
import nostr.postr.JsonFilter import nostr.postr.JsonFilter
import nostr.postr.events.ContactListEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent
import nostr.postr.events.MetadataEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") {
@@ -21,8 +21,8 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
val fullArray = val fullArray =
byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag + byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag +
byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr + byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr +
byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind
return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32) return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32)
} }
@@ -41,7 +41,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
Hex.decode(parts[1]) Hex.decode(parts[1])
ATag(parts[0].toInt(), parts[1], parts[2]) ATag(parts[0].toInt(), parts[1], parts[2])
} catch (t: Throwable) { } catch (t: Throwable) {
Log.w("Address", "Error parsing A Tag: ${atag}: ${t.message}") Log.w("ATag", "Error parsing A Tag: ${atag}: ${t.message}")
null null
} }
} }
@@ -62,7 +62,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) {
} }
} catch (e: Throwable) { } catch (e: Throwable) {
println("Issue trying to Decode NIP19 ${this}: ${e.message}") Log.w( "ATag", "Issue trying to Decode NIP19 ${this}: ${e.message}")
//e.printStackTrace() //e.printStackTrace()
} }
@@ -1,18 +1,18 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
import nostr.postr.events.MetadataEvent
class ChannelCreateEvent ( class ChannelCreateEvent (
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun channelInfo() = try { fun channelInfo() = try {
MetadataEvent.gson.fromJson(content, ChannelData::class.java) MetadataEvent.gson.fromJson(content, ChannelData::class.java)
@@ -35,11 +35,11 @@ class ChannelCreateEvent (
"" ""
} }
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = emptyList<List<String>>() val tags = emptyList<List<String>>()
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) return ChannelCreateEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
} }
@@ -1,16 +1,17 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
class ChannelHideMessageEvent ( class ChannelHideMessageEvent (
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
@@ -19,7 +20,7 @@ class ChannelHideMessageEvent (
fun create(reason: String, messagesToHide: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent { fun create(reason: String, messagesToHide: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent {
val content = reason val content = reason
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = val tags =
messagesToHide?.map { messagesToHide?.map {
listOf("e", it) listOf("e", it)
@@ -27,7 +28,7 @@ class ChannelHideMessageEvent (
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) return ChannelHideMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
} }
} }
@@ -1,16 +1,17 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
class ChannelMessageEvent ( class ChannelMessageEvent (
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) fun channel() = tags.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1) ?: tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
@@ -22,7 +23,7 @@ class ChannelMessageEvent (
fun create(message: String, channel: String, replyTos: List<String>? = null, mentions: List<String>? = null, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent { fun create(message: String, channel: String, replyTos: List<String>? = null, mentions: List<String>? = null, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent {
val content = message val content = message
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf( val tags = mutableListOf(
listOf("e", channel, "", "root") listOf("e", channel, "", "root")
) )
@@ -35,7 +36,7 @@ class ChannelMessageEvent (
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) return ChannelMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
} }
} }
@@ -1,18 +1,18 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
import nostr.postr.events.MetadataEvent
class ChannelMetadataEvent ( class ChannelMetadataEvent (
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
fun channelInfo() = fun channelInfo() =
@@ -33,11 +33,11 @@ class ChannelMetadataEvent (
else else
"" ""
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf( listOf("e", originalChannelIdHex, "", "root") ) val tags = listOf( listOf("e", originalChannelIdHex, "", "root") )
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) return ChannelMetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
} }
} }
@@ -1,27 +1,27 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
class ChannelMuteUserEvent ( class ChannelMuteUserEvent (
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
companion object { companion object {
const val kind = 44 const val kind = 44
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent { fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent {
val content = reason val content = reason
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = val tags =
usersToMute?.map { usersToMute?.map {
listOf("p", it) listOf("p", it)
@@ -29,7 +29,7 @@ class ChannelMuteUserEvent (
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) return ChannelMuteUserEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
} }
} }
@@ -0,0 +1,59 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date
import nostr.postr.Utils
data class Contact(val pubKeyHex: String, val relayUri: String?)
class ContactListEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun follows() = try {
tags.filter { it[0] == "p" }.map { Contact(it[1], it.getOrNull(2)) }
} catch (e: Exception) {
Log.e("ContactListEvent", "can't parse tags as follows: $tags", e)
null
}
fun relayUse() = try {
if (content.isNotEmpty())
gson.fromJson(content, object: TypeToken<Map<String, ReadWrite>>() {}.type)
else
null
} catch (e: Exception) {
Log.e("ContactListEvent", "can't parse content as relay lists: $tags", e)
null
}
companion object {
const val kind = 3
fun create(follows: List<Contact>, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ContactListEvent {
val content = if (relayUse != null)
gson.toJson(relayUse)
else
""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = follows.map {
if (it.relayUri != null)
listOf("p", it.pubKeyHex, it.relayUri)
else
listOf("p", it.pubKeyHex)
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return ContactListEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
data class ReadWrite(val read: Boolean, val write: Boolean)
}
@@ -0,0 +1,30 @@
package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date
import nostr.postr.Utils
class DeletionEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun deleteEvents() = tags.map { it[1] }
companion object {
const val kind = 5
fun create(deleteEvents: List<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): DeletionEvent {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = deleteEvents.map { listOf("e", it) }
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return DeletionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -0,0 +1,197 @@
package com.vitorpamplona.amethyst.service.model
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import com.google.gson.annotations.SerializedName
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import fr.acinq.secp256k1.Hex
import fr.acinq.secp256k1.Secp256k1
import java.lang.reflect.Type
import java.security.MessageDigest
import java.util.Date
import nostr.postr.Utils
import nostr.postr.toHex
open class Event(
val id: HexKey,
@SerializedName("pubkey") val pubKey: HexKey,
@SerializedName("created_at") val createdAt: Long,
val kind: Int,
val tags: List<List<String>>,
val content: String,
val sig: HexKey
) {
fun toJson(): String = gson.toJson(this)
fun generateId(): String {
val rawEvent = listOf(
0,
pubKey,
createdAt,
kind,
tags,
content
)
val rawEventJson = gson.toJson(rawEvent)
return sha256.digest(rawEventJson.toByteArray()).toHexKey()
}
/**
* Checks if the ID is correct and then if the pubKey's secret key signed the event.
*/
fun checkSignature() {
if (!id.contentEquals(generateId())) {
throw Exception(
"""|Unexpected ID.
| Event: ${toJson()}
| Actual ID: ${id}
| Generated: ${generateId()}""".trimIndent()
)
}
if (!secp256k1.verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) {
throw Exception("""Bad signature!""")
}
}
fun hasValidSignature(): Boolean {
if (!id.contentEquals(generateId())) {
return false
}
if (!Secp256k1.get().verifySchnorr(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))) {
return false
}
return true
}
class EventDeserializer : JsonDeserializer<Event> {
override fun deserialize(
json: JsonElement,
typeOfT: Type?,
context: JsonDeserializationContext?
): Event {
val jsonObject = json.asJsonObject
return Event(
id = jsonObject.get("id").asString,
pubKey = jsonObject.get("pubkey").asString,
createdAt = jsonObject.get("created_at").asLong,
kind = jsonObject.get("kind").asInt,
tags = jsonObject.get("tags").asJsonArray.map {
it.asJsonArray.map { s -> s.asString }
},
content = jsonObject.get("content").asString,
sig = jsonObject.get("sig").asString
)
}
}
class EventSerializer : JsonSerializer<Event> {
override fun serialize(
src: Event,
typeOfSrc: Type?,
context: JsonSerializationContext?
): JsonElement {
return JsonObject().apply {
addProperty("id", src.id)
addProperty("pubkey", src.pubKey)
addProperty("created_at", src.createdAt)
addProperty("kind", src.kind)
add("tags", JsonArray().also { jsonTags ->
src.tags.forEach { tag ->
jsonTags.add(JsonArray().also { jsonTagElement ->
tag.forEach { tagElement ->
jsonTagElement.add(tagElement)
}
})
}
})
addProperty("content", src.content)
addProperty("sig", src.sig)
}
}
}
class ByteArrayDeserializer : JsonDeserializer<ByteArray> {
override fun deserialize(
json: JsonElement,
typeOfT: Type?,
context: JsonDeserializationContext?
): ByteArray = Hex.decode(json.asString)
}
class ByteArraySerializer : JsonSerializer<ByteArray> {
override fun serialize(
src: ByteArray,
typeOfSrc: Type?,
context: JsonSerializationContext?
) = JsonPrimitive(src.toHex())
}
companion object {
private val secp256k1 = Secp256k1.get()
val sha256: MessageDigest = MessageDigest.getInstance("SHA-256")
val gson: Gson = GsonBuilder()
.disableHtmlEscaping()
.registerTypeAdapter(Event::class.java, EventSerializer())
.registerTypeAdapter(Event::class.java, EventDeserializer())
.registerTypeAdapter(ByteArray::class.java, ByteArraySerializer())
.registerTypeAdapter(ByteArray::class.java, ByteArrayDeserializer())
.create()
fun fromJson(json: String, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient)
fun fromJson(json: JsonElement, lenient: Boolean = false): Event = gson.fromJson(json, Event::class.java).getRefinedEvent(lenient)
fun Event.getRefinedEvent(lenient: Boolean = false): Event = when (kind) {
ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig)
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
PrivateDmEvent.kind -> PrivateDmEvent(id, pubKey, createdAt, tags, content, sig)
ReactionEvent.kind -> ReactionEvent(id, pubKey, createdAt, tags, content, sig)
RecommendRelayEvent.kind -> RecommendRelayEvent(id, pubKey, createdAt, tags, content, sig, lenient)
ReportEvent.kind -> ReportEvent(id, pubKey, createdAt, tags, content, sig)
RepostEvent.kind -> RepostEvent(id, pubKey, createdAt, tags, content, sig)
TextNoteEvent.kind -> TextNoteEvent(id, pubKey, createdAt, tags, content, sig)
else -> this
}
fun generateId(pubKey: HexKey, createdAt: Long, kind: Int, tags: List<List<String>>, content: String): ByteArray {
val rawEvent = listOf(
0,
pubKey,
createdAt,
kind,
tags,
content
)
val rawEventJson = gson.toJson(rawEvent)
return sha256.digest(rawEventJson.toByteArray())
}
fun create(privateKey: ByteArray, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = Date().time / 1000): Event {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val id = Companion.generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey).toHexKey()
return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig)
}
}
}
@@ -1,17 +1,16 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import java.math.BigDecimal
import nostr.postr.events.Event
class LnZapEvent ( class LnZapEvent (
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
@@ -1,17 +1,18 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
import nostr.postr.toHex import nostr.postr.toHex
class LnZapRequestEvent ( class LnZapRequestEvent (
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.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 zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
@@ -22,10 +23,10 @@ class LnZapRequestEvent (
fun create(originalNote: Event, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { fun create(originalNote: Event, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
val content = "" val content = ""
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf( var tags = listOf(
listOf("e", originalNote.id.toHex()), listOf("e", originalNote.id),
listOf("p", originalNote.pubKey.toHex()), listOf("p", originalNote.pubKey),
listOf("relays") + relays listOf("relays") + relays
) )
if (originalNote is LongTextNoteEvent) { if (originalNote is LongTextNoteEvent) {
@@ -34,19 +35,19 @@ class LnZapRequestEvent (
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
fun create(userHex: String, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { fun create(userHex: String, relays: Set<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent {
val content = "" val content = ""
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf( val tags = listOf(
listOf("p", userHex), listOf("p", userHex),
listOf("relays") + relays listOf("relays") + relays
) )
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
} }
} }
@@ -1,23 +1,23 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
class LongTextNoteEvent( class LongTextNoteEvent(
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): 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 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.toHexKey(), dTag()) fun address() = ATag(kind, pubKey, dTag())
fun topics() = tags.filter { it.firstOrNull() == "t" }.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 title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
@@ -34,7 +34,7 @@ class LongTextNoteEvent(
const val kind = 30023 const val kind = 30023
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent { fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
replyTos?.forEach { replyTos?.forEach {
tags.add(listOf("e", it)) tags.add(listOf("e", it))
@@ -44,7 +44,7 @@ class LongTextNoteEvent(
} }
val id = generateId(pubKey, createdAt, kind, tags, msg) val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig) return LongTextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
} }
} }
} }
@@ -0,0 +1,48 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import com.google.gson.Gson
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date
import nostr.postr.Utils
data class ContactMetaData(
val name: String,
val picture: String,
val about: String,
val nip05: String?)
class MetadataEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun contactMetaData() = try {
gson.fromJson(content, ContactMetaData::class.java)
} catch (e: Exception) {
Log.e("MetadataEvent", "Can't parse $content", e)
null
}
companion object {
const val kind = 0
val gson = Gson()
fun create(contactMetaData: ContactMetaData, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
return create(gson.toJson(contactMetaData), privateKey, createdAt = createdAt)
}
fun create(contactMetaData: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
val content = contactMetaData
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf<List<String>>()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -0,0 +1,86 @@
package com.vitorpamplona.amethyst.service.model
import android.util.Log
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import fr.acinq.secp256k1.Hex
import java.util.Date
import nostr.postr.Utils
import nostr.postr.toHex
class PrivateDmEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
/**
* This may or may not be the actual recipient's pub key. The event is intended to look like a
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used
* for initial messages.
*/
fun recipientPubKey() = tags.firstOrNull { it.firstOrNull() == "p" }?.run { Hex.decode(this[1]).toHexKey() } // makes sure its a valid one
/**
* To be fully compatible with nip-04, we read e-tags that are in violation to nip-18.
*
* Nip-18 messages should refer to other events by inline references in the content like
* `[](e/c06f795e1234a9a1aecc731d768d4f3ca73e80031734767067c82d67ce82e506).
*/
fun replyTo() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1)
fun plainContent(privKey: ByteArray, pubKey: ByteArray): String? {
return try {
val sharedSecret = Utils.getSharedSecret(privKey, pubKey)
val retVal = Utils.decrypt(content, sharedSecret)
if (retVal.startsWith(nip18Advertisement)) {
retVal.substring(16)
} else {
retVal
}
} catch (e: Exception) {
Log.w("PrivateDM", "Error decrypting the message ${e.message}")
null
}
}
companion object {
const val kind = 4
const val nip18Advertisement = "[//]: # (nip18)\n"
fun create(
recipientPubKey: ByteArray,
msg: String,
replyTos: List<String>? = null, mentions: List<String>? = null,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000,
publishedRecipientPubKey: ByteArray? = null,
advertiseNip18: Boolean = true
): PrivateDmEvent {
val content = Utils.encrypt(
if (advertiseNip18) { nip18Advertisement } else { "" } + msg,
privateKey,
recipientPubKey)
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
publishedRecipientPubKey?.let {
tags.add(listOf("p", publishedRecipientPubKey.toHex()))
}
replyTos?.forEach {
tags.add(listOf("e", it))
}
mentions?.forEach {
tags.add(listOf("p", it))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return PrivateDmEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,17 +1,18 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
import nostr.postr.toHex import nostr.postr.toHex
class ReactionEvent ( class ReactionEvent (
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun originalPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
@@ -30,16 +31,16 @@ class ReactionEvent (
} }
fun create(content: String, originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent { fun create(content: String, originalNote: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf( listOf("e", originalNote.id.toHex()), listOf("p", originalNote.pubKey.toHex())) var tags = listOf( listOf("e", originalNote.id), listOf("p", originalNote.pubKey))
if (originalNote is LongTextNoteEvent) { if (originalNote is LongTextNoteEvent) {
tags = tags + listOf( listOf("a", originalNote.address().toTag()) ) tags = tags + listOf( listOf("a", originalNote.address().toTag()) )
} }
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ReactionEvent(id, pubKey, createdAt, tags, content, sig) return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
} }
} }
@@ -0,0 +1,37 @@
package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.net.URI
import java.util.Date
import nostr.postr.Utils
class RecommendRelayEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey,
val lenient: Boolean = false
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
fun relay() = if (lenient)
URI.create(content.trim())
else
URI.create(content)
companion object {
const val kind = 2
fun create(relay: URI, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RecommendRelayEvent {
val content = relay.toString()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf<List<String>>()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return RecommendRelayEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
}
}
@@ -1,20 +1,21 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
import nostr.postr.toHex import nostr.postr.toHex
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType) data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
// NIP 56 event. // NIP 56 event.
class ReportEvent ( class ReportEvent (
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
private fun defaultReportType(): ReportType { private fun defaultReportType(): ReportType {
@@ -55,10 +56,10 @@ class ReportEvent (
fun create(reportedPost: Event, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { fun create(reportedPost: Event, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
val content = "" val content = ""
val reportPostTag = listOf("e", reportedPost.id.toHex(), type.name.toLowerCase()) val reportPostTag = listOf("e", reportedPost.id, type.name.toLowerCase())
val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex(), type.name.toLowerCase()) val reportAuthorTag = listOf("p", reportedPost.pubKey, type.name.toLowerCase())
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag) var tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
if (reportedPost is LongTextNoteEvent) { if (reportedPost is LongTextNoteEvent) {
@@ -67,7 +68,7 @@ class ReportEvent (
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ReportEvent(id, pubKey, createdAt, tags, content, sig) return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
@@ -75,11 +76,11 @@ class ReportEvent (
val reportAuthorTag = listOf("p", reportedUser, type.name.toLowerCase()) val reportAuthorTag = listOf("p", reportedUser, type.name.toLowerCase())
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags:List<List<String>> = listOf(reportAuthorTag) val tags:List<List<String>> = listOf(reportAuthorTag)
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return ReportEvent(id, pubKey, createdAt, tags, content, sig) return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
} }
@@ -1,18 +1,19 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
import nostr.postr.toHex import nostr.postr.toHex
class RepostEvent ( class RepostEvent (
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
@@ -32,10 +33,10 @@ class RepostEvent (
fun create(boostedPost: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent { fun create(boostedPost: Event, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent {
val content = boostedPost.toJson() val content = boostedPost.toJson()
val replyToPost = listOf("e", boostedPost.id.toHex()) val replyToPost = listOf("e", boostedPost.id)
val replyToAuthor = listOf("p", boostedPost.pubKey.toHex()) val replyToAuthor = listOf("p", boostedPost.pubKey)
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags:List<List<String>> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor)) var tags:List<List<String>> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor))
if (boostedPost is LongTextNoteEvent) { if (boostedPost is LongTextNoteEvent) {
@@ -44,7 +45,7 @@ class RepostEvent (
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return RepostEvent(id, pubKey, createdAt, tags, content, sig) return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
} }
} }
} }
@@ -1,16 +1,17 @@
package com.vitorpamplona.amethyst.service.model package com.vitorpamplona.amethyst.service.model
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey
import java.util.Date import java.util.Date
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.events.Event
class TextNoteEvent( class TextNoteEvent(
id: ByteArray, id: HexKey,
pubKey: ByteArray, pubKey: HexKey,
createdAt: Long, createdAt: Long,
tags: List<List<String>>, tags: List<List<String>>,
content: String, content: String,
sig: ByteArray sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig) { ): Event(id, pubKey, createdAt, kind, tags, content, sig) {
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 { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) }
@@ -20,7 +21,7 @@ class TextNoteEvent(
const val kind = 1 const val kind = 1
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, addresses: List<ATag>?, 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 pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
replyTos?.forEach { replyTos?.forEach {
tags.add(listOf("e", it)) tags.add(listOf("e", it))
@@ -33,7 +34,7 @@ class TextNoteEvent(
} }
val id = generateId(pubKey, createdAt, kind, tags, msg) val id = generateId(pubKey, createdAt, kind, tags, msg)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return TextNoteEvent(id, pubKey, createdAt, tags, msg, sig) return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey())
} }
} }
} }
@@ -4,7 +4,7 @@ import java.util.UUID
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import nostr.postr.events.Event import com.vitorpamplona.amethyst.service.model.Event
/** /**
* The Nostr Client manages multiple personae the user may switch between. Events are received and * The Nostr Client manages multiple personae the user may switch between. Events are received and
@@ -38,9 +38,7 @@ object Client: RelayPool.Listener {
if (relays.size != newRelayConfig.size) return false if (relays.size != newRelayConfig.size) return false
relays.forEach { oldRelayInfo -> relays.forEach { oldRelayInfo ->
val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url } val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url } ?: return false
if (newRelayInfo == null) return false
if (!oldRelayInfo.isSameRelayConfig(newRelayInfo)) return false if (!oldRelayInfo.isSameRelayConfig(newRelayInfo)) return false
} }
@@ -1,16 +0,0 @@
package com.vitorpamplona.amethyst.service.relays
import fr.acinq.secp256k1.Secp256k1
import nostr.postr.events.Event
import nostr.postr.events.generateId
fun Event.hasValidSignature(): Boolean {
if (!id.contentEquals(generateId())) {
return false
}
if (!Secp256k1.get().verifySchnorr(sig, id, pubKey)) {
return false
}
return true
}
@@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service.relays
import android.util.Log import android.util.Log
import com.google.gson.JsonElement import com.google.gson.JsonElement
import java.util.Date import java.util.Date
import nostr.postr.events.Event import com.vitorpamplona.amethyst.service.model.Event
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
@@ -5,7 +5,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import nostr.postr.events.Event import com.vitorpamplona.amethyst.service.model.Event
/** /**
* RelayPool manages the connection to multiple Relays and lets consumers deal with simple events. * RelayPool manages the connection to multiple Relays and lets consumers deal with simple events.
@@ -38,7 +38,9 @@ import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
@@ -51,8 +53,9 @@ import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Following import com.vitorpamplona.amethyst.ui.theme.Following
import kotlin.time.ExperimentalTime import kotlin.time.ExperimentalTime
import nostr.postr.events.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -85,6 +88,7 @@ fun NoteCompose(
var moreActionsExpanded by remember { mutableStateOf(false) } var moreActionsExpanded by remember { mutableStateOf(false) }
val noteEvent = note?.event val noteEvent = note?.event
val baseChannel = note?.channel()
if (noteEvent == null) { if (noteEvent == null) {
BlankNote(modifier.combinedClickable( BlankNote(modifier.combinedClickable(
@@ -100,6 +104,8 @@ fun NoteCompose(
navController, navController,
onClick = { showHiddenNote = true } onClick = { showHiddenNote = true }
) )
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
ChannelHeader(baseChannel = baseChannel, account = account, navController = navController)
} else { } else {
var isNew by remember { mutableStateOf<Boolean>(false) } var isNew by remember { mutableStateOf<Boolean>(false) }
@@ -134,9 +140,11 @@ fun NoteCompose(
launchSingleTop = true launchSingleTop = true
} }
} else { } else {
note.channel()?.let { note
navController.navigate("Channel/${it.idHex}") .channel()
} ?.let {
navController.navigate("Channel/${it.idHex}")
}
} }
}, },
onLongClick = { popupExpanded = true } onLongClick = { popupExpanded = true }
@@ -176,7 +184,6 @@ fun NoteCompose(
} }
// boosted picture // boosted picture
val baseChannel = note.channel()
if (noteEvent is ChannelMessageEvent && baseChannel != null) { if (noteEvent is ChannelMessageEvent && baseChannel != null) {
val channelState by baseChannel.live.observeAsState() val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel val channel = channelState?.channel
@@ -121,7 +121,6 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
Column(Modifier.fillMaxHeight()) { Column(Modifier.fillMaxHeight()) {
ChannelHeader( ChannelHeader(
channel, account, channel, account,
accountStateViewModel = accountStateViewModel,
navController = navController navController = navController
) )
@@ -213,7 +212,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
} }
@Composable @Composable
fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel: AccountStateViewModel, navController: NavController) { fun ChannelHeader(baseChannel: Channel, account: Account, navController: NavController) {
val channelState by baseChannel.live.observeAsState() val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel ?: return val channel = channelState?.channel ?: return
@@ -91,7 +91,7 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) {
LaunchedEffect(accountViewModel) { LaunchedEffect(accountViewModel) {
NostrChatroomListDataSource.resetFilters() NostrChatroomListDataSource.resetFilters()
feedViewModel.invalidateData() feedViewModel.refresh()
} }
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
@@ -99,7 +99,7 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) {
val observer = LifecycleEventObserver { source, event -> val observer = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
NostrChatroomListDataSource.resetFilters() NostrChatroomListDataSource.resetFilters()
feedViewModel.invalidateData() feedViewModel.refresh()
} }
} }
@@ -128,7 +128,22 @@ fun TabNew(accountViewModel: AccountViewModel, navController: NavController) {
LaunchedEffect(accountViewModel) { LaunchedEffect(accountViewModel) {
NostrChatroomListDataSource.resetFilters() NostrChatroomListDataSource.resetFilters()
feedViewModel.invalidateData() // refresh view feedViewModel.refresh()
}
val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_RESUME) {
NostrChatroomListDataSource.resetFilters()
feedViewModel.refresh()
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose {
lifeCycleOwner.lifecycle.removeObserver(observer)
}
} }
Column(Modifier.fillMaxHeight()) { Column(Modifier.fillMaxHeight()) {
@@ -79,7 +79,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
LaunchedEffect(userId) { LaunchedEffect(userId) {
feedViewModel.invalidateData() feedViewModel.refresh()
} }
DisposableEffect(userId) { DisposableEffect(userId) {
@@ -87,7 +87,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
println("Private Message Start") println("Private Message Start")
NostrChatroomDataSource.start() NostrChatroomDataSource.start()
feedViewModel.invalidateData() feedViewModel.refresh()
} }
if (event == Lifecycle.Event.ON_PAUSE) { if (event == Lifecycle.Event.ON_PAUSE) {
println("Private Message Stop") println("Private Message Stop")
@@ -55,8 +55,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController)
LaunchedEffect(accountViewModel) { LaunchedEffect(accountViewModel) {
NostrHomeDataSource.resetFilters() NostrHomeDataSource.resetFilters()
feedViewModel.invalidateData() feedViewModel.refresh()
feedViewModelReplies.invalidateData() feedViewModelReplies.refresh()
} }
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
@@ -64,8 +64,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController)
val observer = LifecycleEventObserver { source, event -> val observer = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
NostrHomeDataSource.resetFilters() NostrHomeDataSource.resetFilters()
feedViewModel.invalidateData() feedViewModel.refresh()
feedViewModelReplies.invalidateData() feedViewModelReplies.refresh()
} }
} }
@@ -36,7 +36,7 @@ fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavCon
DisposableEffect(accountViewModel) { DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { source, event -> val observer = LifecycleEventObserver { source, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
feedViewModel.invalidateData() feedViewModel.refresh()
} }
} }
@@ -86,8 +86,8 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle
val feedViewModel: NostrGlobalFeedViewModel = viewModel() val feedViewModel: NostrGlobalFeedViewModel = viewModel()
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) { LaunchedEffect(accountViewModel) {
feedViewModel.invalidateData() feedViewModel.refresh()
} }
DisposableEffect(accountViewModel) { DisposableEffect(accountViewModel) {
@@ -95,7 +95,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
println("Global Start") println("Global Start")
NostrGlobalDataSource.start() NostrGlobalDataSource.start()
feedViewModel.invalidateData() feedViewModel.refresh()
} }
if (event == Lifecycle.Event.ON_PAUSE) { if (event == Lifecycle.Event.ON_PAUSE) {
println("Global Stop") println("Global Stop")