Merge branch 'main' into main

This commit is contained in:
Vitor Pamplona
2023-03-03 18:06:40 -05:00
committed by GitHub
68 changed files with 1195 additions and 483 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId "com.vitorpamplona.amethyst" applicationId "com.vitorpamplona.amethyst"
minSdk 26 minSdk 26
targetSdk 33 targetSdk 33
versionCode 84 versionCode 85
versionName "0.22.1" versionName "0.22.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
+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\\/(.*)")
@@ -1,71 +1,103 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.model.ATag import nostr.postr.bechToBytes
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.ByteOrder import java.nio.ByteOrder
import nostr.postr.Bech32
import nostr.postr.bechToBytes
import nostr.postr.toByteArray
class Nip19 { class Nip19 {
enum class Type { enum class Type {
USER, NOTE, RELAY, ADDRESS USER, NOTE, RELAY, ADDRESS
} }
data class Return(val type: Type, val hex: String) data class Return(val type: Type, val hex: String)
fun uriToRoute(uri: String?): Return? { fun uriToRoute(uri: String?): Return? {
try { try {
val key = uri?.removePrefix("nostr:") val key = uri?.removePrefix("nostr:") ?: return null
if (key != null) { val bytes = key.bechToBytes()
val bytes = key.bechToBytes() if (key.startsWith("npub")) {
if (key.startsWith("npub")) { return npub(bytes)
return Return(Type.USER, bytes.toHexKey()) } else if (key.startsWith("note")) {
} return note(bytes)
if (key.startsWith("note")) { } else if (key.startsWith("nprofile")) {
return Return(Type.NOTE, bytes.toHexKey()) return nprofile(bytes)
} } else if (key.startsWith("nevent")) {
if (key.startsWith("nprofile")) { return nevent(bytes)
val tlv = parseTLV(bytes) } else if (key.startsWith("nrelay")) {
val hex = tlv.get(NIP19TLVTypes.SPECIAL.id)?.get(0)?.toHexKey() return nrelay(bytes)
if (hex != null) } else if (key.startsWith("naddr")) {
return Return(Type.USER, hex) return naddr(bytes)
}
if (key.startsWith("nevent")) {
val tlv = parseTLV(bytes)
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) { } catch (e: Throwable) {
println("Issue trying to Decode NIP19 ${uri}: ${e.message}") println("Issue trying to Decode NIP19 ${uri}: ${e.message}")
//e.printStackTrace()
} }
return null return null
} }
private fun npub(bytes: ByteArray): Return {
return Return(Type.USER, bytes.toHexKey())
}
private fun note(bytes: ByteArray): Return {
return Return(Type.NOTE, bytes.toHexKey());
}
private fun nprofile(bytes: ByteArray): Return? {
val hex = parseTLV(bytes)
.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.toHexKey() ?: return null
return Return(Type.USER, hex)
}
private fun nevent(bytes: ByteArray): Return? {
val hex = parseTLV(bytes)
.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.toHexKey() ?: return null
return Return(Type.USER, hex)
}
private fun nrelay(bytes: ByteArray): Return? {
val relayUrl = parseTLV(bytes)
.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.toString(Charsets.UTF_8) ?: return null
return Return(Type.RELAY, relayUrl)
}
private fun naddr(bytes: ByteArray): Return? {
val tlv = parseTLV(bytes)
val d = tlv.get(NIP19TLVTypes.SPECIAL.id)
?.get(0)
?.toString(Charsets.UTF_8) ?: return null
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) }
return Return(Type.ADDRESS, "$kind:$author:$d")
}
} }
enum class NIP19TLVTypes(val id: Byte) { //classes should start with an uppercase letter in kotlin // Classes should start with an uppercase letter in kotlin
enum class NIP19TLVTypes(val id: Byte) {
SPECIAL(0), SPECIAL(0),
RELAY(1), RELAY(1),
AUTHOR(2), AUTHOR(2),
@@ -78,19 +110,19 @@ fun toInt32(bytes: ByteArray): Int {
} }
fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> { fun parseTLV(data: ByteArray): Map<Byte, List<ByteArray>> {
var result = mutableMapOf<Byte, MutableList<ByteArray>>() val result = mutableMapOf<Byte, MutableList<ByteArray>>()
var rest = data var rest = data
while (rest.isNotEmpty()) { while (rest.isNotEmpty()) {
val t = rest[0] val t = rest[0]
val l = rest[1] val l = rest[1]
val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) val v = rest.sliceArray(IntRange(2, (2 + l) - 1))
rest = rest.sliceArray(IntRange(2 + l, rest.size-1)) rest = rest.sliceArray(IntRange(2 + l, rest.size - 1))
if (v.size < l) continue if (v.size < l) continue
if (!result.containsKey(t)) { if (!result.containsKey(t)) {
result.put(t, mutableListOf()) result[t] = mutableListOf()
} }
result.get(t)?.add(v) result[t]?.add(v)
} }
return result return result
} }
@@ -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.
@@ -7,12 +7,13 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.Icon import androidx.compose.material.Icon
@@ -36,8 +37,8 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontWeight.Companion.W500
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.navigation.NavController import androidx.navigation.NavController
@@ -45,14 +46,14 @@ import androidx.navigation.NavHostController
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.RoboHashCache
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountBackupDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.platform.LocalContext
@Composable @Composable
fun DrawerContent(navController: NavHostController, fun DrawerContent(navController: NavHostController,
@@ -88,7 +89,8 @@ fun DrawerContent(navController: NavHostController,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1F), .weight(1F),
accountStateViewModel accountStateViewModel,
account,
) )
BottomContent(account.userProfile(), scaffoldState, navController) BottomContent(account.userProfile(), scaffoldState, navController)
@@ -155,38 +157,44 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
if (accountUser.bestDisplayName() != null) { if (accountUser.bestDisplayName() != null) {
Text( Text(
accountUser.bestDisplayName() ?: "", accountUser.bestDisplayName() ?: "",
modifier = Modifier.padding(top = 7.dp).clickable(onClick = { modifier = Modifier
accountUser.let { .padding(top = 7.dp)
navController.navigate("User/${it.pubkeyHex}") .clickable(onClick = {
} accountUser.let {
coroutineScope.launch { navController.navigate("User/${it.pubkeyHex}")
scaffoldState.drawerState.close() }
} coroutineScope.launch {
}), scaffoldState.drawerState.close()
}
}),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
fontSize = 18.sp fontSize = 18.sp
) )
} }
if (accountUser.bestUsername() != null) { if (accountUser.bestUsername() != null) {
Text(" @${accountUser.bestUsername()}", color = Color.LightGray, Text(" @${accountUser.bestUsername()}", color = Color.LightGray,
modifier = Modifier.padding(top = 15.dp).clickable(onClick = { modifier = Modifier
accountUser.let { .padding(top = 15.dp)
navController.navigate("User/${it.pubkeyHex}") .clickable(onClick = {
} accountUser.let {
coroutineScope.launch { navController.navigate("User/${it.pubkeyHex}")
scaffoldState.drawerState.close() }
} coroutineScope.launch {
}) scaffoldState.drawerState.close()
}
})
) )
} }
Row(modifier = Modifier.padding(top = 15.dp).clickable(onClick = { Row(modifier = Modifier
accountUser.let { .padding(top = 15.dp)
navController.navigate("User/${it.pubkeyHex}") .clickable(onClick = {
} accountUser.let {
coroutineScope.launch { navController.navigate("User/${it.pubkeyHex}")
scaffoldState.drawerState.close() }
} coroutineScope.launch {
})) { scaffoldState.drawerState.close()
}
})) {
Row() { Row() {
Text("${accountUserFollows.follows.size}", fontWeight = FontWeight.Bold) Text("${accountUserFollows.follows.size}", fontWeight = FontWeight.Bold)
Text(stringResource(R.string.following)) Text(stringResource(R.string.following))
@@ -206,66 +214,84 @@ fun ListContent(
navController: NavHostController, navController: NavHostController,
scaffoldState: ScaffoldState, scaffoldState: ScaffoldState,
modifier: Modifier, modifier: Modifier,
accountViewModel: AccountStateViewModel accountViewModel: AccountStateViewModel,
account: Account,
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
var backupDialogOpen by remember { mutableStateOf(false) }
Column(modifier = modifier) { Column(modifier = modifier.fillMaxHeight()) {
LazyColumn() { if (accountUser != null)
item { NavigationRow(
if (accountUser != null) title = stringResource(R.string.profile),
NavigationRow(navController, icon = Route.Profile.icon,
scaffoldState, tint = MaterialTheme.colors.primary,
"User/${accountUser.pubkeyHex}", navController = navController,
Route.Profile.icon, scaffoldState = scaffoldState,
stringResource(R.string.profile) route = "User/${accountUser.pubkeyHex}",
) )
Divider( Divider(thickness = 0.25.dp)
modifier = Modifier.padding(bottom = 15.dp),
thickness = 0.25.dp NavigationRow(
) title = stringResource(R.string.security_filters),
Column(modifier = modifier.padding(horizontal = 25.dp)) { icon = Route.Filters.icon,
Row(modifier = Modifier.clickable(onClick = { tint = MaterialTheme.colors.onBackground,
navController.navigate(Route.Filters.route) navController = navController,
coroutineScope.launch { scaffoldState = scaffoldState,
scaffoldState.drawerState.close() route = Route.Filters.route,
} )
})) {
Text( Divider(thickness = 0.25.dp)
text = stringResource(R.string.security_filters),
fontSize = 18.sp, IconRow(
fontWeight = W500 title = "Backup Keys",
) icon = R.drawable.ic_key,
} tint = MaterialTheme.colors.onBackground,
Row(modifier = Modifier.clickable(onClick = { accountViewModel.logOff() })) { onClick = { backupDialogOpen = true }
Text( )
text = stringResource(R.string.log_out),
modifier = Modifier.padding(vertical = 15.dp), Spacer(modifier = Modifier.weight(1f))
fontSize = 18.sp,
fontWeight = W500 IconRow(
) "Logout",
} R.drawable.ic_logout,
} MaterialTheme.colors.onBackground,
} onClick = { accountViewModel.logOff() }
} )
}
if (backupDialogOpen) {
AccountBackupDialog(account, onClose = { backupDialogOpen = false })
} }
} }
@Composable @Composable
fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState, route: String, icon: Int, title: String) { fun NavigationRow(
title: String,
icon: Int,
tint: Color,
navController: NavHostController,
scaffoldState: ScaffoldState,
route: String,
) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val currentRoute = currentRoute(navController) val currentRoute = currentRoute(navController)
IconRow(title, icon, tint, onClick = {
if (currentRoute != route) {
navController.navigate(route)
}
coroutineScope.launch {
scaffoldState.drawerState.close()
}
})
}
@Composable
fun IconRow(title: String, icon: Int, tint: Color, onClick: () -> Unit) {
Row(modifier = Modifier Row(modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = { .clickable(onClick = onClick)
if (currentRoute != route) {
navController.navigate(route)
}
coroutineScope.launch {
scaffoldState.drawerState.close()
}
})
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -276,7 +302,7 @@ fun NavigationRow(navController: NavHostController, scaffoldState: ScaffoldState
Icon( Icon(
painter = painterResource(icon), null, painter = painterResource(icon), null,
modifier = Modifier.size(22.dp), modifier = Modifier.size(22.dp),
tint = MaterialTheme.colors.primary tint = tint
) )
Text( Text(
modifier = Modifier.padding(start = 16.dp), modifier = Modifier.padding(start = 16.dp),
@@ -53,7 +53,7 @@ sealed class Route(
buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }} buildScreen = { acc, accSt, nav -> { _ -> ChatroomListScreen(acc, nav) }}
) )
object Filters : Route("Filters", R.drawable.ic_dm, object Filters : Route("Filters", R.drawable.ic_security,
buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }} buildScreen = { acc, accSt, nav -> { _ -> FiltersScreen(acc, nav) }}
) )
@@ -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) }
@@ -178,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
@@ -0,0 +1,123 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Key
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.halilibo.richtext.markdown.Markdown
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.material.MaterialRichText
import com.halilibo.richtext.ui.resolveDefaults
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import kotlinx.coroutines.launch
import nostr.postr.toNsec
@Composable
fun AccountBackupDialog(account: Account, onClose: () -> Unit) {
Dialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.background(MaterialTheme.colors.background)
.fillMaxSize(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = onClose)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
MaterialRichText(
style = RichTextStyle().resolveDefaults(),
) {
Markdown(
content = stringResource(R.string.account_backup_tips_md),
)
}
Spacer(modifier = Modifier.height(15.dp))
NSecCopyButton(account)
}
}
}
}
}
@Composable
private fun NSecCopyButton(
account: Account
) {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
Button(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = {
account.loggedIn.privKey?.let {
clipboardManager.setText(AnnotatedString(it.toNsec()))
scope.launch {
Toast.makeText(
context,
context.getString(R.string.secret_key_copied_to_clipboard),
Toast.LENGTH_SHORT
).show()
}
}
},
shape = RoundedCornerShape(20.dp), colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.primary
)
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.Key,
contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup)
)
Text("Copy Secret Key", color = MaterialTheme.colors.onPrimary)
}
}
@@ -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()
} }
} }
@@ -10,7 +10,6 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
@@ -78,7 +77,6 @@ import com.vitorpamplona.amethyst.ui.screen.UserFeedView
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import nostr.postr.toNsec
@OptIn(ExperimentalPagerApi::class) @OptIn(ExperimentalPagerApi::class)
@Composable @Composable
@@ -337,10 +335,6 @@ private fun ProfileHeader(
.padding(bottom = 3.dp)) { .padding(bottom = 3.dp)) {
MessageButton(baseUser, navController) MessageButton(baseUser, navController)
if (accountUser == baseUser && account.isWriteable()) {
NSecCopyButton(account)
}
NPubCopyButton(baseUser) NPubCopyButton(baseUser)
if (accountUser == baseUser) { if (accountUser == baseUser) {
@@ -637,40 +631,7 @@ fun TabRelays(user: User, accountViewModel: AccountViewModel, navController: Nav
} }
} }
@Composable
private fun NSecCopyButton(
account: Account
) {
val clipboardManager = LocalClipboardManager.current
var popupExpanded by remember { mutableStateOf(false) }
Button(
modifier = Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = { popupExpanded = true },
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.Key,
contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup)
)
DropdownMenu(
expanded = popupExpanded,
onDismissRequest = { popupExpanded = false }
) {
DropdownMenuItem(onClick = { account.loggedIn.privKey?.let { clipboardManager.setText(AnnotatedString(it.toNsec())) }; popupExpanded = false }) {
Text(stringResource(R.string.copy_private_key_to_the_clipboard))
}
}
}
}
@Composable @Composable
private fun NPubCopyButton( private fun NPubCopyButton(
@@ -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")
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
<path
android:fillColor="@android:color/white"
android:pathData="M21,10h-8.35C11.83,7.67 9.61,6 7,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6c2.61,0 4.83,-1.67 5.65,-4H13l2,2l2,-2l2,2l4,-4.04L21,10zM7,15c-1.65,0 -3,-1.35 -3,-3c0,-1.65 1.35,-3 3,-3s3,1.35 3,3C10,13.65 8.65,15 7,15z"/>
</vector>
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M17,7l-1.41,1.41L18.17,11H8v2h10.17l-2.58,2.58L17,17l5,-5zM4,5h8V3H4c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h8v-2H4V5z"/>
</vector>
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#333333"
android:alpha="0.6">
<path
android:fillColor="@android:color/white"
android:pathData="M12,1L3,5v6c0,5.55 3.84,10.74 9,12 5.16,-1.26 9,-6.45 9,-12L21,5l-9,-4zM12,11.99h7c-0.53,4.12 -3.28,7.79 -7,8.94L12,12L5,12L5,6.3l7,-3.11v8.8z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

+10 -10
View File
@@ -24,7 +24,7 @@
<string name="copy_user_pubkey">Copiar PubKey del usuario</string> <string name="copy_user_pubkey">Copiar PubKey del usuario</string>
<string name="copy_note_id">Copiar ID de la nota</string> <string name="copy_note_id">Copiar ID de la nota</string>
<string name="broadcast">Transmisión</string> <string name="broadcast">Transmisión</string>
<string name="block_hide_user">Bloquear y ocultar usuario></string> <string name="block_hide_user">Bloquear y ocultar usuario</string>
<string name="report_spam_scam">Reportar Spam / Estafa</string> <string name="report_spam_scam">Reportar Spam / Estafa</string>
<string name="report_impersonation">Reportar suplantación de identidad</string> <string name="report_impersonation">Reportar suplantación de identidad</string>
<string name="report_explicit_content">Reportar contenido explícito</string> <string name="report_explicit_content">Reportar contenido explícito</string>
@@ -36,7 +36,7 @@
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Inicie sesión con una clave privada para poder enviar Zaps</string> <string name="login_with_a_private_key_to_be_able_to_send_zaps">Inicie sesión con una clave privada para poder enviar Zaps</string>
<string name="zaps">Zaps</string> <string name="zaps">Zaps</string>
<string name="view_count">Total vistas</string> <string name="view_count">Total vistas</string>
<string name="boost">Aumentar</string> <string name="boost">Impulsar</string>
<string name="boosted">boosted</string> <string name="boosted">boosted</string>
<string name="quote">Cita</string> <string name="quote">Cita</string>
<string name="new_amount_in_sats">Nueva cantidad en Sats</string> <string name="new_amount_in_sats">Nueva cantidad en Sats</string>
@@ -47,7 +47,7 @@
<string name="profile_banner">Banner de perfil</string> <string name="profile_banner">Banner de perfil</string>
<string name="following">" Siguiendo"</string> <string name="following">" Siguiendo"</string>
<string name="followers">" Seguidores"</string> <string name="followers">" Seguidores"</string>
<string name="profile">Peril</string> <string name="profile">Perfil</string>
<string name="security_filters">Filtros de seguridad</string> <string name="security_filters">Filtros de seguridad</string>
<string name="log_out">Cerrar sesión</string> <string name="log_out">Cerrar sesión</string>
<string name="show_more">Mostrar más</string> <string name="show_more">Mostrar más</string>
@@ -68,7 +68,7 @@
<string name="description">Descripción</string> <string name="description">Descripción</string>
<string name="about_us">"Sobre nosotros… "</string> <string name="about_us">"Sobre nosotros… "</string>
<string name="what_s_on_your_mind">¿Qué tienes en mente?</string> <string name="what_s_on_your_mind">¿Qué tienes en mente?</string>
<string name="post">Publicación</string> <string name="post">Enviar</string>
<string name="save">Guardar</string> <string name="save">Guardar</string>
<string name="create">Crear</string> <string name="create">Crear</string>
<string name="cancel">Cancelar</string> <string name="cancel">Cancelar</string>
@@ -101,15 +101,15 @@
<string name="copy_channel_id_note_to_the_clipboard">Copia la nota-ID del canal</string> <string name="copy_channel_id_note_to_the_clipboard">Copia la nota-ID del canal</string>
<string name="edits_the_channel_metadata">Edita los metadatos del canal</string> <string name="edits_the_channel_metadata">Edita los metadatos del canal</string>
<string name="join">Unirse</string> <string name="join">Unirse</string>
<string name="known">Conocido</string> <string name="known">Conocidos</string>
<string name="new_requests">Nuevas solicitudes</string> <string name="new_requests">Nuevas solicitudes</string>
<string name="blocked_users">Usuarios bloqueados</string> <string name="blocked_users">Usuarios bloqueados</string>
<string name="new_threads">Temas nuevos</string> <string name="new_threads">Temas nuevos</string>
<string name="conversations">Conversaciones</string> <string name="conversations">Conversaciones</string>
<string name="notes">Notas</string> <string name="notes">Notas</string>
<string name="replies">Respuestas</string> <string name="replies">Respuestas</string>
<string name="follows">"Sigue"</string> <string name="follows">Siguiendo</string>
<string name="reports">"Reportes"</string> <string name="reports">Reportes</string>
<string name="more_options">Más opciones</string> <string name="more_options">Más opciones</string>
<string name="relays">" Retransmisores"</string> <string name="relays">" Retransmisores"</string>
<string name="website">Sitio web</string> <string name="website">Sitio web</string>
@@ -137,7 +137,7 @@
<string name="key_is_required">Se requiere la clave</string> <string name="key_is_required">Se requiere la clave</string>
<string name="login">Acceso</string> <string name="login">Acceso</string>
<string name="generate_a_new_key">Generar una nueva clave</string> <string name="generate_a_new_key">Generar una nueva clave</string>
<string name="loading_feed">Cargando el tablón</string> <string name="loading_feed">Cargando el tablón</string>
<string name="error_loading_replies">"Error al cargar las respuestas: "</string> <string name="error_loading_replies">"Error al cargar las respuestas: "</string>
<string name="try_again">Intentar otra vez</string> <string name="try_again">Intentar otra vez</string>
<string name="feed_is_empty">Tablón vacío</string> <string name="feed_is_empty">Tablón vacío</string>
@@ -151,7 +151,7 @@
<string name="leave">Dejar</string> <string name="leave">Dejar</string>
<string name="unfollow">Dejar de seguir</string> <string name="unfollow">Dejar de seguir</string>
<string name="channel_created">Canal creado</string> <string name="channel_created">Canal creado</string>
<string name="channel_information_changed_to">"La información del canal cambió a"</string> <string name="channel_information_changed_to">La información del canal cambió a</string>
<string name="public_chat">Chat público</string> <string name="public_chat">Chat público</string>
<string name="posts_received">publicaciones recibidas</string> <string name="posts_received">publicaciones recibidas</string>
<string name="remove">Eliminar</string> <string name="remove">Eliminar</string>
@@ -161,7 +161,7 @@
<string name="to">a</string> <string name="to">a</string>
<string name="show_in">Mostrar en</string> <string name="show_in">Mostrar en</string>
<string name="first">primero</string> <string name="first">primero</string>
<string name="always_translate_to">Traducir siempre a</string> <string name="always_translate_to">"Traducir siempre a "</string>
<string name="nip_05" translatable="false">NIP-05</string> <string name="nip_05" translatable="false">NIP-05</string>
<string name="lnurl" translatable="false">LNURL...</string> <string name="lnurl" translatable="false">LNURL...</string>
<string name="never">nunca</string> <string name="never">nunca</string>
+9 -1
View File
@@ -114,7 +114,7 @@
<string name="website">Website</string> <string name="website">Website</string>
<string name="lightning_address">Lightning Address</string> <string name="lightning_address">Lightning Address</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copies the Nsec ID (your password) to the clipboard for backup</string> <string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copies the Nsec ID (your password) to the clipboard for backup</string>
<string name="copy_private_key_to_the_clipboard">Copy Private Key to the Clipboard</string> <string name="copy_private_key_to_the_clipboard">Copy Secret Key to the Clipboard</string>
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Copies the public key to the clipboard for sharing</string> <string name="copies_the_public_key_to_the_clipboard_for_sharing">Copies the public key to the clipboard for sharing</string>
<string name="copy_public_key_npub_to_the_clipboard">Copy Public Key (NPub) to the Clipboard</string> <string name="copy_public_key_npub_to_the_clipboard">Copy Public Key (NPub) to the Clipboard</string>
<string name="send_a_direct_message">Send a Direct Message</string> <string name="send_a_direct_message">Send a Direct Message</string>
@@ -173,4 +173,12 @@
<string name="report_hateful_speech">Report Hateful speech</string> <string name="report_hateful_speech">Report Hateful speech</string>
<string name="report_nudity_porn">Report Nudity / Porn</string> <string name="report_nudity_porn">Report Nudity / Porn</string>
<string name="others">others</string> <string name="others">others</string>
<string name="account_backup_tips_md">
## Key Backup and Safety Tips
\n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity.
\n\n- Do **not** put your secret key in any website or software you do not trust.
\n- Amethyst developers will **never** ask you for your secret key.
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
</string>
<string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string>
</resources> </resources>
@@ -0,0 +1,109 @@
package com.vitorpamplona.amethyst.service
import org.junit.Assert
import org.junit.Ignore
import org.junit.Test
class Nip19Test {
private val nip19 = Nip19();
@Test(expected = IllegalArgumentException::class)
fun to_int_32_length_smaller_than_4() {
toInt32(byteArrayOfInts(1, 2, 3))
}
@Test(expected = IllegalArgumentException::class)
fun to_int_32_length_bigger_than_4() {
toInt32(byteArrayOfInts(1, 2, 3, 4, 5))
}
@Test()
fun to_int_32_length_4() {
val actual = toInt32(byteArrayOfInts(1, 2, 3, 4))
Assert.assertEquals(16909060, actual)
}
@Ignore("Test not implemented yet")
@Test()
fun parse_TLV() {
// TODO: I don't know how to test this (?)
}
@Test()
fun uri_to_route_null() {
val actual = nip19.uriToRoute(null)
Assert.assertEquals(null, actual)
}
@Test()
fun uri_to_route_unknown() {
val actual = nip19.uriToRoute("nostr:unknown")
Assert.assertEquals(null, actual)
}
@Test()
fun uri_to_route_npub() {
val actual =
nip19.uriToRoute("nostr:npub1hv7k2s755n697sptva8vkh9jz40lzfzklnwj6ekewfmxp5crwdjs27007y")
Assert.assertEquals(Nip19.Type.USER, actual?.type)
Assert.assertEquals(
"bb3d6543d4a4f45f402b674ecb5cb2155ff12456fcdd2d66d9727660d3037365",
actual?.hex
)
}
@Test()
fun uri_to_route_note() {
val actual =
nip19.uriToRoute("nostr:note1stqea6wmwezg9x6yyr6qkukw95ewtdukyaztycws65l8wppjmtpscawevv")
Assert.assertEquals(Nip19.Type.NOTE, actual?.type)
Assert.assertEquals(
"82c19ee9db7644829b4420f40b72ce2d32e5b7962744b261d0d53e770432dac3",
actual?.hex
)
}
@Ignore("Test not implemented yet")
@Test()
fun uri_to_route_nprofile() {
val actual = nip19.uriToRoute("nostr:nprofile")
Assert.assertEquals(Nip19.Type.USER, actual?.type)
Assert.assertEquals("*", actual?.hex)
}
@Ignore("Test not implemented yet")
@Test()
fun uri_to_route_nevent() {
val actual = nip19.uriToRoute("nostr:nevent")
Assert.assertEquals(Nip19.Type.USER, actual?.type)
Assert.assertEquals("*", actual?.hex)
}
@Ignore("Test not implemented yet")
@Test()
fun uri_to_route_nrelay() {
val actual = nip19.uriToRoute("nostr:nrelay")
Assert.assertEquals(Nip19.Type.RELAY, actual?.type)
Assert.assertEquals("*", actual?.hex)
}
@Ignore("Test not implemented yet")
@Test()
fun uri_to_route_naddr() {
val actual = nip19.uriToRoute("nostr:naddr")
Assert.assertEquals(Nip19.Type.ADDRESS, actual?.type)
Assert.assertEquals("*", actual?.hex)
}
private fun byteArrayOfInts(vararg ints: Int) = ByteArray(ints.size) { pos -> ints[pos].toByte() }
}