diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index bbee40884..5acc1b972 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -25,7 +25,7 @@ -keep class fr.acinq.secp256k1.jni.** { *; } # For the NostrPostr library -keep class nostr.postr.** { *; } --keep class nostr.postr.events.** { *; } +-keep class com.vitorpamplona.amethyst.service.model.** { *; } # Json parsing -keep class com.google.gson.reflect.** { *; } -keep class * extends com.google.gson.reflect.TypeToken diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 8684b11d1..38be7f0da 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -8,9 +8,9 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo import com.vitorpamplona.amethyst.model.toByteArray import java.util.Locale import nostr.postr.Persona -import nostr.postr.events.ContactListEvent -import nostr.postr.events.Event -import nostr.postr.events.Event.Companion.getRefinedEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.Event +import com.vitorpamplona.amethyst.service.model.Event.Companion.getRefinedEvent import nostr.postr.toHex class LocalPreferences(context: Context) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index ed433bebf..b91282926 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.LiveData import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent 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.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent @@ -26,16 +27,12 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.Contact import nostr.postr.Persona -import nostr.postr.Utils -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 com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.DeletionEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent -import nostr.postr.toHex val DefaultChannels = setOf( "25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb", // -> Anigma's Nostr @@ -89,10 +86,11 @@ class Account( if (!isWriteable()) return 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( - contactList.follows, + follows, relays, loggedIn.privKey!!) @@ -111,14 +109,7 @@ class Account( if (!isWriteable()) return loggedIn.privKey?.let { - val createdAt = Date().time / 1000 - val content = toString - val pubKey = Utils.pubkeyCreate(it) - val tags = listOf>() - 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) - + val event = MetadataEvent.create(toString, loggedIn.privKey!!) Client.send(event) LocalCache.consume(event) } @@ -250,10 +241,11 @@ class Account( if (!isWriteable()) return 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( - contactList.follows.plus(Contact(user.pubkeyHex, null)), + follows.plus(Contact(user.pubkeyHex, null)), userProfile().relays, loggedIn.privKey!!) } else { @@ -273,10 +265,11 @@ class Account( if (!isWriteable()) return 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( - contactList.follows.filter { it.pubKeyHex != user.pubkeyHex }, + follows.filter { it.pubKeyHex != user.pubkeyHex }, userProfile().relays, loggedIn.privKey!!) @@ -320,37 +313,6 @@ class Account( LocalCache.consume(signedEvent, null) } - fun createPrivateMessageWithReply( - recipientPubKey: ByteArray, - msg: String, - replyTos: List? = null, mentions: List? = 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>() - 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) { if (!isWriteable()) return val user = LocalCache.users[toUser] ?: return @@ -358,7 +320,7 @@ class Account( val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val mentionsHex = emptyList() - val signedEvent = createPrivateMessageWithReply( + val signedEvent = PrivateDmEvent.create( recipientPubKey = user.pubkey(), publishedRecipientPubKey = user.pubkey(), msg = message, @@ -386,7 +348,7 @@ class Account( Client.send(event) LocalCache.consume(event) - joinChannel(event.id.toHex()) + joinChannel(event.id) } fun joinChannel(idHex: String) { @@ -438,7 +400,7 @@ class Account( Client.send(event) LocalCache.consume(event) - joinChannel(event.id.toHex()) + joinChannel(event.id) } fun decryptContent(note: Note): String? { @@ -446,26 +408,12 @@ class Account( return if (event is PrivateDmEvent && loggedIn.privKey != null) { var pubkeyToUse = event.pubKey - val recepientPK = event.recipientPubKey + val recepientPK = event.recipientPubKey() if (note.author == userProfile() && recepientPK != null) pubkeyToUse = recepientPK - return try { - 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 - } + event.plainContent(loggedIn.privKey!!, pubkeyToUse.toByteArray()) } else { event?.content } @@ -495,10 +443,10 @@ class Account( } 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. - if (backupContactList?.id?.toHex() != newContactList?.id?.toHex()) { + if (backupContactList?.id != newContactList?.id) { backupContactList = newContactList saveable.invalidateData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt index 3fec4edf8..a4bb82eae 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/AntiSpamFilter.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event import nostr.postr.toHex data class Spammer(val pubkeyHex: HexKey, var duplicatedMessages: Set) @@ -22,7 +22,7 @@ class AntiSpamFilter { @Synchronized fun isSpam(event: Event): Boolean { - val idHex = event.id.toHexKey() + val idHex = event.id // if already processed, ok if (LocalCache.notes[idHex] != null) return false @@ -37,15 +37,15 @@ class AntiSpamFilter { val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode() 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 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() } else { val spammer = spamMessages.get(hash) - spammer.duplicatedMessages = spammer.duplicatedMessages + event.id.toHex() + spammer.duplicatedMessages = spammer.duplicatedMessages + event.id liveSpam.invalidateData() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 66b388368..ae3f07104 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -17,6 +17,13 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent 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 fr.acinq.secp256k1.Hex import java.io.ByteArrayInputStream @@ -32,13 +39,6 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch 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.toNpub @@ -139,7 +139,7 @@ object LocalCache { fun consume(event: MetadataEvent) { // new event - val oldUser = getOrCreateUser(event.pubKey.toHexKey()) + val oldUser = getOrCreateUser(event.pubKey) if (oldUser.info == null || event.createdAt > oldUser.info!!.updatedMetadataAt) { val newUser = try { metadataParser.readValue( @@ -173,8 +173,8 @@ object LocalCache { return } - val note = getOrCreateNote(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -220,7 +220,7 @@ object LocalCache { } val note = getOrCreateAddressableNote(event.address()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -228,7 +228,7 @@ object LocalCache { } // 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 replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) } @@ -284,14 +284,15 @@ object LocalCache { } 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 user.latestContactList = event user.updateFollows( - event.follows.map { + follows.map { try { val pubKey = decodePublicKey(it.pubKeyHex) getOrCreateUser(pubKey.toHexKey()) @@ -316,20 +317,17 @@ object LocalCache { user.updateRelays(relays) } } catch (e: Exception) { - println("relay import issue") + Log.w("Relay List Parser","Relay import issue ${e.message}", e) e.printStackTrace() } - Log.d( - "CL", - "AAA ${user.toBestDisplayName()} ${event.follows.size}" - ) + Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}") } } fun consume(event: PrivateDmEvent, relay: Relay?) { - val note = getOrCreateNote(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -339,7 +337,7 @@ object LocalCache { // Already processed this event. 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()}") @@ -359,9 +357,9 @@ object LocalCache { fun consume(event: DeletionEvent) { var deletedAtLeastOne = false - event.deleteEvents.mapNotNull { notes[it] }.forEach { deleteNote -> + event.deleteEvents().mapNotNull { notes[it] }.forEach { deleteNote -> // must be the same author - if (deleteNote.author?.pubkeyHex == event.pubKey.toHexKey()) { + if (deleteNote.author?.pubkeyHex == event.pubKey) { deleteNote.author?.removeNote(deleteNote) // reverts the add @@ -395,14 +393,14 @@ object LocalCache { } fun consume(event: RepostEvent) { - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return //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 repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } @@ -429,12 +427,12 @@ object LocalCache { } fun consume(event: ReactionEvent) { - val note = getOrCreateNote(event.id.toHexKey()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) val mentions = event.originalAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.originalPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } @@ -475,8 +473,8 @@ object LocalCache { } fun consume(event: ReportEvent, relay: Relay?) { - val note = getOrCreateNote(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val note = getOrCreateNote(event.id) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -507,13 +505,13 @@ object LocalCache { fun consume(event: ChannelCreateEvent) { //Log.d("MT", "New Event ${event.content} ${event.id.toHex()}") // new event - val oldChannel = getOrCreateChannel(event.id.toHex()) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val oldChannel = getOrCreateChannel(event.id) + val author = getOrCreateUser(event.pubKey) if (event.createdAt > oldChannel.updatedMetadataAt) { if (oldChannel.creator == null || oldChannel.creator == author) { oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) oldChannel.addNote(note) note.loadEvent(event, author, emptyList(), emptyList()) @@ -530,12 +528,12 @@ object LocalCache { // new event val oldChannel = checkGetOrCreateChannel(channelId) ?: return - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) if (event.createdAt > oldChannel.updatedMetadataAt) { if (oldChannel.creator == null || oldChannel.creator == author) { oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt) - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) oldChannel.addNote(note) note.loadEvent(event, author, emptyList(), emptyList()) @@ -559,10 +557,10 @@ object LocalCache { val channel = checkGetOrCreateChannel(channelId) ?: return - val note = getOrCreateNote(event.id.toHex()) + val note = getOrCreateNote(event.id) channel.addNote(note) - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) if (relay != null) { author.addRelayBeingUsed(relay, event.createdAt) @@ -606,14 +604,14 @@ object LocalCache { } fun consume(event: LnZapEvent) { - val note = getOrCreateNote(event.id.toHexKey()) + val note = getOrCreateNote(event.id) // Already processed this event. 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 repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } + @@ -645,15 +643,15 @@ object LocalCache { } fun consume(event: LnZapRequestEvent) { - val note = getOrCreateNote(event.id.toHexKey()) + val note = getOrCreateNote(event.id) // Already processed this event. if (note.event != null) return - val author = getOrCreateUser(event.pubKey.toHexKey()) + val author = getOrCreateUser(event.pubKey) val mentions = event.zappedAuthor().mapNotNull { checkGetOrCreateUser(it) } val repliesTo = event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + - event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } note.loadEvent(event, author, mentions, repliesTo) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index 12d130b8a..892792119 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -27,7 +27,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") @@ -72,7 +72,7 @@ open class Note(val idHex: String) { val channelHex = (event as? ChannelMessageEvent)?.channel() ?: (event as? ChannelMetadataEvent)?.channel() ?: - (event as? ChannelCreateEvent)?.let { it.id.toHexKey() } + (event as? ChannelCreateEvent)?.let { it.id } return channelHex?.let { LocalCache.checkGetOrCreateChannel(it) } } @@ -251,6 +251,22 @@ open class Note(val idHex: String) { }?.isNotEmpty() ?: false) } + fun directlyCiteUsersHex(): Set { + val matcher = tagSearch.matcher(event?.content ?: "") + val returningList = mutableSetOf() + 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 { val matcher = tagSearch.matcher(event?.content ?: "") val returningList = mutableSetOf() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt index 1b7de0513..d0f0e41f0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/User.kt @@ -18,8 +18,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import nostr.postr.Bech32 -import nostr.postr.events.ContactListEvent -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import nostr.postr.toNpub val lnurlpPattern = Pattern.compile("(?i:http|https):\\/\\/((.+)\\/)*\\.well-known\\/lnurlp\\/(.*)") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index e7958ff7c..daa2fe6b6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -9,8 +9,8 @@ import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.ContactListEvent -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrAccountDataSource: NostrDataSource("AccountData") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt index 1e08e1bea..7ad19edad 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomDataSource.kt @@ -6,7 +6,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent object NostrChatroomDataSource: NostrDataSource("ChatroomFeed") { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt index e1e721344..cdf92ad24 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrChatroomListDataSource.kt @@ -7,7 +7,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.PrivateDmEvent +import com.vitorpamplona.amethyst.service.model.PrivateDmEvent object NostrChatroomListDataSource: NostrDataSource("MailBoxFeed") { lateinit var account: Account diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 2956fdd7c..ffdde932b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.service +import android.util.Log import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent 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.Relay import com.vitorpamplona.amethyst.service.relays.Subscription -import com.vitorpamplona.amethyst.service.relays.hasValidSignature import java.util.Date import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean @@ -26,12 +26,12 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.launch 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.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 abstract class NostrDataSource(val debugName: String) { @@ -62,39 +62,32 @@ abstract class NostrDataSource(val debugName: String) { try { when (event) { - is MetadataEvent -> LocalCache.consume(event) - //is TextNoteEvent -> LocalCache.consume(event, relay) overrides default TextNote - is RecommendRelayEvent -> LocalCache.consume(event) + is ChannelCreateEvent -> LocalCache.consume(event) + is ChannelHideMessageEvent -> 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 PrivateDmEvent -> LocalCache.consume(event, relay) 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 -> { - val repostEvent = RepostEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) - - repostEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) } - LocalCache.consume(repostEvent) - } - ReactionEvent.kind -> LocalCache.consume(ReactionEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - ReportEvent.kind -> LocalCache.consume(ReportEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay) - - LnZapEvent.kind -> { - val zapEvent = LnZapEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig) - - zapEvent.containedPost()?.let { onEvent(it, subscriptionId, relay) } - LocalCache.consume(zapEvent) - } - LnZapRequestEvent.kind -> LocalCache.consume(LnZapRequestEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - - ChannelCreateEvent.kind -> LocalCache.consume(ChannelCreateEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig)) - 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) + is LnZapEvent -> { + event.containedPost()?.let { onEvent(it, subscriptionId, relay) } + LocalCache.consume(event) + } + is LnZapRequestEvent -> LocalCache.consume(event) + is LongTextNoteEvent -> LocalCache.consume(event, relay) + is MetadataEvent -> LocalCache.consume(event) + is PrivateDmEvent -> LocalCache.consume(event, relay) + is ReactionEvent -> LocalCache.consume(event) + is RecommendRelayEvent -> LocalCache.consume(event) + is ReportEvent -> LocalCache.consume(event, relay) + is RepostEvent -> { + event.containedPost()?.let { onEvent(it, subscriptionId, relay) } + LocalCache.consume(event) + } + is TextNoteEvent -> LocalCache.consume(event, relay) + else -> { + Log.w("Event Not Supported", event.toJson()) } } } catch (e: Exception) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt index dee1eea3a..6f3abc42f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSearchEventOrUserDataSource.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter import nostr.postr.bechToBytes -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import nostr.postr.toHex object NostrSearchEventOrUserDataSource: NostrDataSource("SingleEventFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt index 387b08a1c..823d1d159 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrSingleUserDataSource.kt @@ -5,7 +5,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent object NostrSingleUserDataSource: NostrDataSource("SingleUserFeed") { var usersToWatch = setOf() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt index 18fbafe2f..04ce33011 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrUserProfileDataSource.kt @@ -7,8 +7,8 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.TypedFilter import nostr.postr.JsonFilter -import nostr.postr.events.ContactListEvent -import nostr.postr.events.MetadataEvent +import com.vitorpamplona.amethyst.service.model.ContactListEvent +import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent object NostrUserProfileDataSource: NostrDataSource("UserProfileFeed") { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt index 0ecfd8f5d..4a8ae0374 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt @@ -21,8 +21,8 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { val fullArray = byteArrayOf(NIP19TLVTypes.SPECIAL.id, dTag.size.toByte()) + dTag + - byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr + - byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind + byteArrayOf(NIP19TLVTypes.AUTHOR.id, addr.size.toByte()) + addr + + byteArrayOf(NIP19TLVTypes.KIND.id, kind.size.toByte()) + kind return Bech32.encodeBytes(hrp = "naddr", fullArray, Bech32.Encoding.Bech32) } @@ -41,7 +41,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { Hex.decode(parts[1]) ATag(parts[0].toInt(), parts[1], parts[2]) } catch (t: Throwable) { - Log.w("Address", "Error parsing A Tag: ${atag}: ${t.message}") + Log.w("ATag", "Error parsing A Tag: ${atag}: ${t.message}") null } } @@ -62,7 +62,7 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String) { } } 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() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt index 791aafecf..6a1d93f93 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt @@ -1,18 +1,18 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent class ChannelCreateEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun channelInfo() = try { 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>() val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelCreateEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt index 41c526e4f..4f8b57f18 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt @@ -1,16 +1,17 @@ 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 -import nostr.postr.events.Event class ChannelHideMessageEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun eventsToHide() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } @@ -19,7 +20,7 @@ class ChannelHideMessageEvent ( fun create(reason: String, messagesToHide: List?, mentions: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent { val content = reason - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = messagesToHide?.map { listOf("e", it) @@ -27,7 +28,7 @@ class ChannelHideMessageEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelHideMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt index 39f0e6bae..411a258d8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt @@ -1,16 +1,17 @@ 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 -import nostr.postr.events.Event class ChannelMessageEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): 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) @@ -22,7 +23,7 @@ class ChannelMessageEvent ( fun create(message: String, channel: String, replyTos: List? = null, mentions: List? = null, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMessageEvent { val content = message - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf( listOf("e", channel, "", "root") ) @@ -35,7 +36,7 @@ class ChannelMessageEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt index 2552ad89c..d84ea2ab8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt @@ -1,18 +1,18 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.toHexKey import java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event -import nostr.postr.events.MetadataEvent class ChannelMetadataEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun channel() = tags.firstOrNull { it.firstOrNull() == "e" }?.getOrNull(1) fun channelInfo() = @@ -33,11 +33,11 @@ class ChannelMetadataEvent ( else "" - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = listOf( listOf("e", originalChannelIdHex, "", "root") ) val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelMetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt index 23c1c52fd..6b12e96bf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt @@ -1,27 +1,27 @@ 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 -import nostr.postr.events.Event class ChannelMuteUserEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun usersToMute() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } - companion object { const val kind = 44 fun create(reason: String, usersToMute: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent { val content = reason - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = usersToMute?.map { listOf("p", it) @@ -29,7 +29,7 @@ class ChannelMuteUserEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig) + return ChannelMuteUserEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt new file mode 100644 index 000000000..502fb7551 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt @@ -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>, + 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>() {}.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, relayUse: Map?, 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) +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt new file mode 100644 index 000000000..a29cd6261 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt @@ -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>, + 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, 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()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt new file mode 100644 index 000000000..6ef8f2e24 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -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>, + 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 { + 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 { + 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 { + override fun deserialize( + json: JsonElement, + typeOfT: Type?, + context: JsonDeserializationContext? + ): ByteArray = Hex.decode(json.asString) + } + + class ByteArraySerializer : JsonSerializer { + 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>, 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> = 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index 2a38c8cf4..6b43b627b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -1,17 +1,16 @@ package com.vitorpamplona.amethyst.service.model +import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil import com.vitorpamplona.amethyst.service.relays.Client -import java.math.BigDecimal -import nostr.postr.events.Event class LnZapEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index 049b4b434..e17c6e672 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -1,17 +1,18 @@ 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 -import nostr.postr.events.Event import nostr.postr.toHex class LnZapRequestEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun zappedPost() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) } fun zappedAuthor() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } @@ -22,10 +23,10 @@ class LnZapRequestEvent ( fun create(originalNote: Event, relays: Set, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { val content = "" - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags = listOf( - listOf("e", originalNote.id.toHex()), - listOf("p", originalNote.pubKey.toHex()), + listOf("e", originalNote.id), + listOf("p", originalNote.pubKey), listOf("relays") + relays ) if (originalNote is LongTextNoteEvent) { @@ -34,19 +35,19 @@ class LnZapRequestEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) 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, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LnZapRequestEvent { val content = "" - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = listOf( listOf("p", userHex), listOf("relays") + relays ) val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig) + return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt index 037c23c83..14fd66d47 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt @@ -1,23 +1,23 @@ 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 -import nostr.postr.events.Event class LongTextNoteEvent( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun replyTos() = tags.filter { it.firstOrNull() == "e" }.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 address() = ATag(kind, pubKey.toHexKey(), dTag()) + fun address() = ATag(kind, pubKey, dTag()) fun topics() = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) } fun title() = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull() @@ -34,7 +34,7 @@ class LongTextNoteEvent( const val kind = 30023 fun create(msg: String, replyTos: List?, mentions: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent { - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() replyTos?.forEach { tags.add(listOf("e", it)) @@ -44,7 +44,7 @@ class LongTextNoteEvent( } val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = Utils.sign(id, privateKey) - return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig) + return LongTextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt new file mode 100644 index 000000000..bd4c4c6a6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt @@ -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>, + 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>() + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt new file mode 100644 index 000000000..d0f5111f1 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt @@ -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>, + 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? = null, mentions: List? = 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>() + 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()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index 6048a0978..c01972758 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -1,17 +1,18 @@ 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 -import nostr.postr.events.Event import nostr.postr.toHex class ReactionEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { 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 { - 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) { tags = tags + listOf( listOf("a", originalNote.address().toTag()) ) } val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ReactionEvent(id, pubKey, createdAt, tags, content, sig) + return ReactionEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt new file mode 100644 index 000000000..a2ef2eaec --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt @@ -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>, + 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>() + val id = generateId(pubKey, createdAt, kind, tags, content) + val sig = Utils.sign(id, privateKey) + return RecommendRelayEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index a1d83c155..4eebabae6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -1,20 +1,21 @@ 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 -import nostr.postr.events.Event import nostr.postr.toHex data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType) // NIP 56 event. class ReportEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { private fun defaultReportType(): ReportType { @@ -55,10 +56,10 @@ class ReportEvent ( fun create(reportedPost: Event, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent { val content = "" - val reportPostTag = listOf("e", reportedPost.id.toHex(), type.name.toLowerCase()) - val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex(), type.name.toLowerCase()) + val reportPostTag = listOf("e", reportedPost.id, 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> = listOf(reportPostTag, reportAuthorTag) if (reportedPost is LongTextNoteEvent) { @@ -67,7 +68,7 @@ class ReportEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) 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 { @@ -75,11 +76,11 @@ class ReportEvent ( val reportAuthorTag = listOf("p", reportedUser, type.name.toLowerCase()) - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags:List> = listOf(reportAuthorTag) val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return ReportEvent(id, pubKey, createdAt, tags, content, sig) + return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index 3da165f2c..74756c6d0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -1,18 +1,19 @@ 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 java.util.Date import nostr.postr.Utils -import nostr.postr.events.Event import nostr.postr.toHex class RepostEvent ( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): 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 { val content = boostedPost.toJson() - val replyToPost = listOf("e", boostedPost.id.toHex()) - val replyToAuthor = listOf("p", boostedPost.pubKey.toHex()) + val replyToPost = listOf("e", boostedPost.id) + val replyToAuthor = listOf("p", boostedPost.pubKey) - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() var tags:List> = boostedPost.tags.plus(listOf(replyToPost, replyToAuthor)) if (boostedPost is LongTextNoteEvent) { @@ -44,7 +45,7 @@ class RepostEvent ( val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) - return RepostEvent(id, pubKey, createdAt, tags, content, sig) + return RepostEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt index 9e1634b16..63839802b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt @@ -1,16 +1,17 @@ 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 -import nostr.postr.events.Event class TextNoteEvent( - id: ByteArray, - pubKey: ByteArray, + id: HexKey, + pubKey: HexKey, createdAt: Long, tags: List>, content: String, - sig: ByteArray + sig: HexKey ): Event(id, pubKey, createdAt, kind, tags, content, sig) { fun mentions() = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) } fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull { it.getOrNull(1) }.mapNotNull { ATag.parse(it) } @@ -20,7 +21,7 @@ class TextNoteEvent( const val kind = 1 fun create(msg: String, replyTos: List?, mentions: List?, addresses: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): TextNoteEvent { - val pubKey = Utils.pubkeyCreate(privateKey) + val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() replyTos?.forEach { tags.add(listOf("e", it)) @@ -33,7 +34,7 @@ class TextNoteEvent( } val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = Utils.sign(id, privateKey) - return TextNoteEvent(id, pubKey, createdAt, tags, msg, sig) + return TextNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) } } } \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt index 555d5d83a..776a670fb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Client.kt @@ -4,7 +4,7 @@ import java.util.UUID import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope 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 @@ -38,9 +38,7 @@ object Client: RelayPool.Listener { if (relays.size != newRelayConfig.size) return false relays.forEach { oldRelayInfo -> - val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url } - - if (newRelayInfo == null) return false + val newRelayInfo = newRelayConfig.find { it.url == oldRelayInfo.url } ?: return false if (!oldRelayInfo.isSameRelayConfig(newRelayInfo)) return false } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt deleted file mode 100644 index 3bbab0e16..000000000 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/EventVerifier.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 214f5c1ae..dc28b944e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -3,7 +3,7 @@ package com.vitorpamplona.amethyst.service.relays import android.util.Log import com.google.gson.JsonElement import java.util.Date -import nostr.postr.events.Event +import com.vitorpamplona.amethyst.service.model.Event import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt index 7b96ca7ea..bb9952a82 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/RelayPool.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job 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. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 8c353220a..9fd223e2b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -38,7 +38,9 @@ import com.vitorpamplona.amethyst.RoboHashCache import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note 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.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent 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.theme.Following 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.ui.screen.loggedIn.ChannelHeader @OptIn(ExperimentalFoundationApi::class) @Composable @@ -85,6 +88,7 @@ fun NoteCompose( var moreActionsExpanded by remember { mutableStateOf(false) } val noteEvent = note?.event + val baseChannel = note?.channel() if (noteEvent == null) { BlankNote(modifier.combinedClickable( @@ -100,6 +104,8 @@ fun NoteCompose( navController, onClick = { showHiddenNote = true } ) + } else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) { + ChannelHeader(baseChannel = baseChannel, account = account, navController = navController) } else { var isNew by remember { mutableStateOf(false) } @@ -134,9 +140,11 @@ fun NoteCompose( launchSingleTop = true } } else { - note.channel()?.let { - navController.navigate("Channel/${it.idHex}") - } + note + .channel() + ?.let { + navController.navigate("Channel/${it.idHex}") + } } }, onLongClick = { popupExpanded = true } @@ -176,7 +184,6 @@ fun NoteCompose( } // boosted picture - val baseChannel = note.channel() if (noteEvent is ChannelMessageEvent && baseChannel != null) { val channelState by baseChannel.live.observeAsState() val channel = channelState?.channel diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 55f9d219f..7fc6aff8b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -121,7 +121,6 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun Column(Modifier.fillMaxHeight()) { ChannelHeader( channel, account, - accountStateViewModel = accountStateViewModel, navController = navController ) @@ -213,7 +212,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun } @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 channel = channelState?.channel ?: return diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt index 0087b96ef..d8959fd62 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomListScreen.kt @@ -91,7 +91,7 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { LaunchedEffect(accountViewModel) { NostrChatroomListDataSource.resetFilters() - feedViewModel.invalidateData() + feedViewModel.refresh() } val lifeCycleOwner = LocalLifecycleOwner.current @@ -99,7 +99,7 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) { val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_RESUME) { NostrChatroomListDataSource.resetFilters() - feedViewModel.invalidateData() + feedViewModel.refresh() } } @@ -128,7 +128,22 @@ fun TabNew(accountViewModel: AccountViewModel, navController: NavController) { LaunchedEffect(accountViewModel) { 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()) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index 1edbc6155..17bda6fcf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -79,7 +79,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr val lifeCycleOwner = LocalLifecycleOwner.current LaunchedEffect(userId) { - feedViewModel.invalidateData() + feedViewModel.refresh() } DisposableEffect(userId) { @@ -87,7 +87,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr if (event == Lifecycle.Event.ON_RESUME) { println("Private Message Start") NostrChatroomDataSource.start() - feedViewModel.invalidateData() + feedViewModel.refresh() } if (event == Lifecycle.Event.ON_PAUSE) { println("Private Message Stop") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt index 5b91d4c31..466ad2959 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HomeScreen.kt @@ -55,8 +55,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) LaunchedEffect(accountViewModel) { NostrHomeDataSource.resetFilters() - feedViewModel.invalidateData() - feedViewModelReplies.invalidateData() + feedViewModel.refresh() + feedViewModelReplies.refresh() } val lifeCycleOwner = LocalLifecycleOwner.current @@ -64,8 +64,8 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController) val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_RESUME) { NostrHomeDataSource.resetFilters() - feedViewModel.invalidateData() - feedViewModelReplies.invalidateData() + feedViewModel.refresh() + feedViewModelReplies.refresh() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt index 6221913c7..ca0724285 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/NotificationScreen.kt @@ -36,7 +36,7 @@ fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavCon DisposableEffect(accountViewModel) { val observer = LifecycleEventObserver { source, event -> if (event == Lifecycle.Event.ON_RESUME) { - feedViewModel.invalidateData() + feedViewModel.refresh() } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt index 7c66c1afe..65e5eb519 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SearchScreen.kt @@ -86,8 +86,8 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle val feedViewModel: NostrGlobalFeedViewModel = viewModel() val lifeCycleOwner = LocalLifecycleOwner.current - LaunchedEffect(Unit) { - feedViewModel.invalidateData() + LaunchedEffect(accountViewModel) { + feedViewModel.refresh() } DisposableEffect(accountViewModel) { @@ -95,7 +95,7 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle if (event == Lifecycle.Event.ON_RESUME) { println("Global Start") NostrGlobalDataSource.start() - feedViewModel.invalidateData() + feedViewModel.refresh() } if (event == Lifecycle.Event.ON_PAUSE) { println("Global Stop")