From 6303573dd9647f75bd7164ca259770f08ecd6a34 Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 6 Sep 2023 18:53:11 -0300 Subject: [PATCH] support for sending and receiving nip 44 dms --- .../vitorpamplona/amethyst/model/Account.kt | 236 ++++++++++++++---- .../amethyst/service/AmberUtils.kt | 22 +- .../amethyst/service/IntentUtils.kt | 4 + .../service/NostrAccountDataSource.kt | 33 +++ .../EventNotificationConsumer.kt | 60 ++++- .../amethyst/ui/actions/SignerDialog.kt | 4 +- .../ui/dal/BookmarkPrivateFeedFilter.kt | 2 +- .../amethyst/ui/note/ChatroomHeaderCompose.kt | 3 +- .../ui/note/ChatroomMessageCompose.kt | 3 +- .../amethyst/ui/note/NoteCompose.kt | 3 +- .../amethyst/ui/note/UserProfilePicture.kt | 12 +- .../ui/screen/loggedIn/ChatroomScreen.kt | 2 - .../quartz/events/ChatMessageEvent.kt | 16 +- .../quartz/events/GiftWrapEvent.kt | 25 ++ .../quartz/events/NIP24Factory.kt | 16 +- .../quartz/events/SealedGossipEvent.kt | 43 ++++ 16 files changed, 399 insertions(+), 85 deletions(-) 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 e7641c5be..12661f185 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -16,6 +16,7 @@ import com.vitorpamplona.amethyst.service.relays.Constants import com.vitorpamplona.amethyst.service.relays.FeedType import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.RelayPool +import com.vitorpamplona.amethyst.ui.actions.SignerType import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.note.combineWith import com.vitorpamplona.quartz.crypto.CryptoUtils @@ -114,7 +115,8 @@ class Account( AmberUtils.content = "" AmberUtils.decrypt( content, - keyPair.pubKey.toHexKey() + keyPair.pubKey.toHexKey(), + blockList?.id() ?: "" ) blockList?.decryptedContent = AmberUtils.content live.invalidateData() @@ -282,7 +284,7 @@ class Account( return } - if (note.event is ChatMessageEvent && !loginWithAmber) { + if (note.event is ChatMessageEvent) { val event = note.event as ChatMessageEvent val users = event.recipientsPubKey().plus(event.pubKey).toSet().toList() @@ -290,14 +292,48 @@ class Account( val emojiUrl = EmojiUrl.decode(reaction) if (emojiUrl != null) { note.event?.let { - val giftWraps = NIP24Factory().createReactionWithinGroup( - emojiUrl = emojiUrl, - originalNote = it, - to = users, - from = keyPair - ) + if (loginWithAmber) { + val senderPublicKey = keyPair.pubKey.toHexKey() - broadcastPrivately(giftWraps) + var senderReaction = ReactionEvent.create( + emojiUrl, + it, + keyPair + ) + + AmberUtils.openAmber(senderReaction) + if (AmberUtils.content.isBlank()) return + senderReaction = ReactionEvent.create(senderReaction, AmberUtils.content) + + val giftWraps = users.plus(senderPublicKey).map { + val gossip = Gossip.create(senderReaction) + val content = Gossip.toJson(gossip) + AmberUtils.encrypt(content, it, SignerType.NIP44_ENCRYPT) + + var sealedEvent = SealedGossipEvent.create( + encryptedContent = AmberUtils.content, + pubKey = senderPublicKey + ) + AmberUtils.openAmber(sealedEvent) + if (AmberUtils.content.isBlank()) return + sealedEvent = SealedGossipEvent.create(sealedEvent, AmberUtils.content) + + GiftWrapEvent.create( + event = sealedEvent, + recipientPubKey = it + ) + } + + broadcastPrivately(giftWraps) + } else { + val giftWraps = NIP24Factory().createReactionWithinGroup( + emojiUrl = emojiUrl, + originalNote = it, + to = users, + from = keyPair + ) + broadcastPrivately(giftWraps) + } } return @@ -305,14 +341,51 @@ class Account( } note.event?.let { - val giftWraps = NIP24Factory().createReactionWithinGroup( - content = reaction, - originalNote = it, - to = users, - from = keyPair - ) + if (loginWithAmber) { + val senderPublicKey = keyPair.pubKey.toHexKey() - broadcastPrivately(giftWraps) + var senderReaction = ReactionEvent.create( + reaction, + it, + keyPair + ) + + AmberUtils.openAmber(senderReaction) + if (AmberUtils.content.isBlank()) return + senderReaction = ReactionEvent.create(senderReaction, AmberUtils.content) + + val newUsers = users.plus(senderPublicKey) + newUsers.forEach { + val gossip = Gossip.create(senderReaction) + val content = Gossip.toJson(gossip) + AmberUtils.content = "" + AmberUtils.encrypt(content, it, SignerType.NIP44_ENCRYPT) + + var sealedEvent = SealedGossipEvent.create( + encryptedContent = AmberUtils.content, + pubKey = senderPublicKey + ) + AmberUtils.openAmber(sealedEvent) + if (AmberUtils.content.isBlank()) return + sealedEvent = SealedGossipEvent.create(sealedEvent, AmberUtils.content) + + val giftWraps = GiftWrapEvent.create( + event = sealedEvent, + recipientPubKey = it + ) + + broadcastPrivately(listOf(giftWraps)) + } + } else { + val giftWraps = NIP24Factory().createReactionWithinGroup( + content = reaction, + originalNote = it, + to = users, + from = keyPair + ) + + broadcastPrivately(giftWraps) + } } return } else { @@ -1373,32 +1446,67 @@ class Account( wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null - ): List? { - // TODO: add support for amber - if (!isWriteable() && !loginWithAmber) return null + ) { + if (!isWriteable() && !loginWithAmber) return val repliesToHex = listOfNotNull(replyingTo?.idHex).ifEmpty { null } val mentionsHex = mentions?.map { it.pubkeyHex } - val signedEvents = NIP24Factory().createMsgNIP24( - msg = message, - to = toUsers, - subject = subject, - replyTos = repliesToHex, - mentions = mentionsHex, - zapReceiver = zapReceiver, - markAsSensitive = wantsToMarkAsSensitive, - zapRaiserAmount = zapRaiserAmount, - geohash = geohash, - from = keyPair.privKey!! - ) - if (loginWithAmber) { - return signedEvents - } + var chatMessageEvent = ChatMessageEvent.create( + msg = message, + to = toUsers, + keyPair = keyPair, + subject = subject, + replyTos = repliesToHex, + mentions = mentionsHex, + zapReceiver = zapReceiver, + markAsSensitive = wantsToMarkAsSensitive, + zapRaiserAmount = zapRaiserAmount, + geohash = geohash + ) - broadcastPrivately(signedEvents) - return null + AmberUtils.openAmber(chatMessageEvent) + if (AmberUtils.content.isBlank()) return + chatMessageEvent = ChatMessageEvent.create(chatMessageEvent, AmberUtils.content) + val senderPublicKey = keyPair.pubKey.toHexKey() + toUsers.plus(senderPublicKey).toSet().forEach { + val gossip = Gossip.create(chatMessageEvent) + val content = Gossip.toJson(gossip) + AmberUtils.content = "" + AmberUtils.encrypt(content, it, SignerType.NIP44_ENCRYPT) + if (AmberUtils.content.isNotBlank()) { + var sealedEvent = SealedGossipEvent.create( + encryptedContent = AmberUtils.content, + pubKey = senderPublicKey + ) + AmberUtils.openAmber(sealedEvent) + if (AmberUtils.content.isBlank()) return + sealedEvent = SealedGossipEvent.create(sealedEvent, AmberUtils.content) + + val giftWraps = GiftWrapEvent.create( + event = sealedEvent, + recipientPubKey = it + ) + broadcastPrivately(listOf(giftWraps)) + } + } + } else { + val signedEvents = NIP24Factory().createMsgNIP24( + msg = message, + to = toUsers, + subject = subject, + replyTos = repliesToHex, + mentions = mentionsHex, + zapReceiver = zapReceiver, + markAsSensitive = wantsToMarkAsSensitive, + zapRaiserAmount = zapRaiserAmount, + geohash = geohash, + keyPair = keyPair + ) + + broadcastPrivately(signedEvents) + } } fun broadcastPrivately(signedEvents: List) { @@ -1407,13 +1515,33 @@ class Account( // Only keep in cache the GiftWrap for the account. if (it.recipientPubKey() == keyPair.pubKey.toHexKey()) { - it.cachedGift(keyPair.privKey!!)?.let { - if (it is SealedGossipEvent) { - it.cachedGossip(keyPair.privKey!!)?.let { + if (loginWithAmber) { + AmberUtils.content = "" + AmberUtils.decrypt(it.content, it.pubKey, it.id, SignerType.NIP44_DECRYPT) + val decryptedContent = AmberUtils.cachedDecryptedContent[it.id] ?: "" + if (decryptedContent.isEmpty()) return + it.cachedGift(keyPair.pubKey, decryptedContent)?.let { cached -> + if (cached is SealedGossipEvent) { + AmberUtils.content = "" + AmberUtils.decrypt(cached.content, cached.pubKey, cached.id, SignerType.NIP44_DECRYPT) + val localDecryptedContent = AmberUtils.cachedDecryptedContent[cached.id] ?: "" + if (localDecryptedContent.isEmpty()) return + cached.cachedGossip(keyPair.pubKey, localDecryptedContent)?.let { gossip -> + LocalCache.justConsume(gossip, null) + } + } else { + LocalCache.justConsume(it, null) + } + } + } else { + it.cachedGift(keyPair.privKey!!)?.let { + if (it is SealedGossipEvent) { + it.cachedGossip(keyPair.privKey!!)?.let { + LocalCache.justConsume(it, null) + } + } else { LocalCache.justConsume(it, null) } - } else { - LocalCache.justConsume(it, null) } } @@ -2026,7 +2154,7 @@ class Account( AmberUtils.content } else { AmberUtils.content = "" - AmberUtils.decrypt(content, keyPair.pubKey.toHexKey()) + AmberUtils.decrypt(content, keyPair.pubKey.toHexKey(), blockList?.id ?: "") if (AmberUtils.content.isBlank()) return val decryptedContent = AmberUtils.content AmberUtils.content = "" @@ -2110,7 +2238,7 @@ class Account( AmberUtils.content } else { AmberUtils.content = "" - AmberUtils.decrypt(content, keyPair.pubKey.toHexKey()) + AmberUtils.decrypt(content, keyPair.pubKey.toHexKey(), blockList.id) if (AmberUtils.content.isBlank()) return val decryptedContent = AmberUtils.content AmberUtils.content = "" @@ -2356,12 +2484,30 @@ class Account( } fun unwrap(event: GiftWrapEvent): Event? { - if (!isWriteable()) return null + if (!isWriteable() && !loginWithAmber) return null + + if (loginWithAmber) { + AmberUtils.content = "" + AmberUtils.decrypt(event.content, event.pubKey, event.id, SignerType.NIP44_DECRYPT) + val decryptedContent = AmberUtils.cachedDecryptedContent[event.id] ?: "" + if (decryptedContent.isEmpty()) return null + return event.cachedGift(keyPair.pubKey, decryptedContent) + } + return event.cachedGift(keyPair.privKey!!) } fun unseal(event: SealedGossipEvent): Event? { - if (!isWriteable()) return null + if (!isWriteable() && !loginWithAmber) return null + + if (loginWithAmber) { + AmberUtils.content = "" + AmberUtils.decrypt(event.content, event.pubKey, event.id, SignerType.NIP44_DECRYPT) + val decryptedContent = AmberUtils.cachedDecryptedContent[event.id] ?: "" + if (decryptedContent.isEmpty()) return null + return event.cachedGossip(keyPair.pubKey, decryptedContent) + } + return event.cachedGossip(keyPair.privKey!!) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/AmberUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/AmberUtils.kt index ea02263d0..efb793795 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/AmberUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/AmberUtils.kt @@ -17,7 +17,8 @@ object AmberUtils { data: String, type: SignerType, intentResult: ActivityResultLauncher, - pubKey: HexKey + pubKey: HexKey, + id: String ) { ServiceManager.shouldPauseService = false val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$data")) @@ -31,6 +32,7 @@ object AmberUtils { } intent.putExtra("type", signerType) intent.putExtra("pubKey", pubKey) + intent.putExtra("id", id) intent.`package` = "com.greenart7c3.nostrsigner.debug" intentResult.launch(intent) } @@ -44,7 +46,8 @@ object AmberUtils { event.toJson(), SignerType.SIGN_EVENT, IntentUtils.activityResultLauncher, - "" + "", + event.id() ) while (isActivityRunning) { // do nothing @@ -59,6 +62,7 @@ object AmberUtils { "", SignerType.GET_PUBLIC_KEY, IntentUtils.activityResultLauncher, + "", "" ) while (isActivityRunning) { @@ -66,14 +70,15 @@ object AmberUtils { } } - fun decrypt(encryptedContent: String, pubKey: HexKey) { + fun decrypt(encryptedContent: String, pubKey: HexKey, id: String, signerType: SignerType = SignerType.NIP04_DECRYPT) { if (content.isBlank()) { isActivityRunning = true openAmber( encryptedContent, - SignerType.NIP04_DECRYPT, + signerType, IntentUtils.activityResultLauncher, - pubKey + pubKey, + id ) while (isActivityRunning) { // do nothing @@ -81,14 +86,15 @@ object AmberUtils { } } - fun encrypt(decryptedContent: String, pubKey: HexKey) { + fun encrypt(decryptedContent: String, pubKey: HexKey, signerType: SignerType = SignerType.NIP04_ENCRYPT) { if (content.isBlank()) { isActivityRunning = true openAmber( decryptedContent, - SignerType.NIP04_ENCRYPT, + signerType, IntentUtils.activityResultLauncher, - pubKey + pubKey, + "" ) while (isActivityRunning) { // do nothing diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/IntentUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/IntentUtils.kt index 3a9e5c227..36bb9a356 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/IntentUtils.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/IntentUtils.kt @@ -34,6 +34,10 @@ object IntentUtils { } val event = it.data?.getStringExtra("signature") ?: "" + val id = it.data?.getStringExtra("id") ?: "" + if (id.isNotBlank()) { + AmberUtils.cachedDecryptedContent[id] = event + } AmberUtils.content = event AmberUtils.isActivityRunning = false ServiceManager.shouldPauseService = true 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 a7dadcb49..7b94661a1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -8,6 +8,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount import com.vitorpamplona.amethyst.service.relays.JsonFilter import com.vitorpamplona.amethyst.service.relays.Relay import com.vitorpamplona.amethyst.service.relays.TypedFilter +import com.vitorpamplona.amethyst.ui.actions.SignerType import com.vitorpamplona.quartz.events.AdvertisedRelayListEvent import com.vitorpamplona.quartz.events.BadgeAwardEvent import com.vitorpamplona.quartz.events.BadgeProfilesEvent @@ -29,6 +30,10 @@ import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.events.StatusEvent import com.vitorpamplona.quartz.events.TextNoteEvent +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch object NostrAccountDataSource : NostrDataSource("AccountData") { lateinit var account: Account @@ -144,6 +149,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time) } + @OptIn(DelicateCoroutinesApi::class) override fun consume(event: Event, relay: Relay) { if (LocalCache.justVerify(event)) { if (event is GiftWrapEvent) { @@ -152,6 +158,17 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { event.cachedGift(privateKey)?.let { this.consume(it, relay) } + } else if (account.loginWithAmber) { + GlobalScope.launch(Dispatchers.IO) { + AmberUtils.content = "" + AmberUtils.decrypt(event.content, event.pubKey, event.id, SignerType.NIP44_DECRYPT) + val decryptedContent = AmberUtils.cachedDecryptedContent[event.id] ?: "" + if (decryptedContent.isNotBlank()) { + event.cachedGift(account.keyPair.pubKey, decryptedContent)?.let { + consume(it, relay) + } + } + } } } @@ -161,6 +178,22 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { event.cachedGossip(privateKey)?.let { LocalCache.justConsume(it, relay) } + } else if (account.loginWithAmber) { + GlobalScope.launch(Dispatchers.IO) { + AmberUtils.content = "" + AmberUtils.decrypt( + event.content, + event.pubKey, + event.id, + SignerType.NIP44_DECRYPT + ) + val decryptedContent = AmberUtils.cachedDecryptedContent[event.id] ?: "" + if (decryptedContent.isNotBlank()) { + event.cachedGossip(account.keyPair.pubKey, decryptedContent)?.let { + LocalCache.justConsume(it, relay) + } + } + } } // Don't store sealed gossips to avoid rebroadcasting by mistake. diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt index 9b740b2f6..16bb213f8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/notifications/EventNotificationConsumer.kt @@ -7,8 +7,10 @@ import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache +import com.vitorpamplona.amethyst.service.AmberUtils import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification +import com.vitorpamplona.amethyst.ui.actions.SignerType import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.events.ChatMessageEvent @@ -47,17 +49,59 @@ class EventNotificationConsumer(private val applicationContext: Context) { return when (event) { is GiftWrapEvent -> { - val key = account.keyPair.privKey ?: return null - event.cachedGift(key)?.let { - unwrapAndConsume(it, account) + val key = account.keyPair.privKey + if (key != null) { + event.cachedGift(key)?.let { + unwrapAndConsume(it, account) + } + } else if (account.loginWithAmber) { + AmberUtils.content = "" + AmberUtils.decrypt( + event.content, + event.pubKey, + event.id, + SignerType.NIP44_DECRYPT + ) + val decryptedContent = AmberUtils.cachedDecryptedContent[event.id] ?: "" + if (decryptedContent.isNotBlank()) { + event.cachedGift(account.keyPair.pubKey, decryptedContent)?.let { + LocalCache.justConsume(it, null) + it + } + } else { + null + } + } else { + null } } is SealedGossipEvent -> { - val key = account.keyPair.privKey ?: return null - event.cachedGossip(key)?.let { - // this is not verifiable - LocalCache.justConsume(it, null) - it + val key = account.keyPair.privKey + if (key != null) { + event.cachedGossip(key)?.let { + // this is not verifiable + LocalCache.justConsume(it, null) + it + } + } else if (account.loginWithAmber) { + AmberUtils.content = "" + AmberUtils.decrypt( + event.content, + event.pubKey, + event.id, + SignerType.NIP44_DECRYPT + ) + val decryptedContent = AmberUtils.cachedDecryptedContent[event.id] ?: "" + if (decryptedContent.isNotBlank()) { + event.cachedGossip(account.keyPair.pubKey, decryptedContent)?.let { + LocalCache.justConsume(it, null) + it + } + } else { + null + } + } else { + null } } else -> { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SignerDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SignerDialog.kt index 3739352ab..93bb68109 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SignerDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/SignerDialog.kt @@ -87,7 +87,7 @@ fun SignerDialog( ) LaunchedEffect(Unit) { - AmberUtils.openAmber(data, type, intentResult, pubKey) + AmberUtils.openAmber(data, type, intentResult, pubKey, "") } Dialog( @@ -178,7 +178,7 @@ fun SignerDialog( ) Button( shape = ButtonBorder, - onClick = { AmberUtils.openAmber(data, type, intentResult, pubKey) } + onClick = { AmberUtils.openAmber(data, type, intentResult, pubKey, "") } ) { Text("Open Amber") } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt index fdd1b1a8c..06c49af25 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/BookmarkPrivateFeedFilter.kt @@ -18,7 +18,7 @@ object BookmarkPrivateFeedFilter : FeedFilter() { if (account.loginWithAmber) { if (AmberUtils.content.isBlank()) { - AmberUtils.decrypt(bookmarks?.content ?: "", account.keyPair.pubKey.toHexKey()) + AmberUtils.decrypt(bookmarks?.content ?: "", account.keyPair.pubKey.toHexKey(), "") bookmarks?.decryptedContent = AmberUtils.content } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt index 92e1b7210..1b180d330 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomHeaderCompose.kt @@ -301,7 +301,8 @@ private fun UserRoomCompose( event?.content() ?: "", SignerType.NIP04_DECRYPT, activityLauncher, - (event as PrivateDmEvent).talkingWith(accountViewModel.userProfile().pubkeyHex) + (event as PrivateDmEvent).talkingWith(accountViewModel.userProfile().pubkeyHex), + event.id ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index da488fc3d..be63e0cd5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -668,7 +668,8 @@ private fun RenderRegularTextNote( event?.content() ?: "", SignerType.NIP04_DECRYPT, activityLauncher, - (event as PrivateDmEvent).talkingWith(accountViewModel.userProfile().pubkeyHex) + (event as PrivateDmEvent).talkingWith(accountViewModel.userProfile().pubkeyHex), + event.id ) } } 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 1c0d12fca..ab4f194c8 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 @@ -1596,7 +1596,8 @@ private fun RenderPrivateMessage( event?.content() ?: "", SignerType.NIP04_DECRYPT, activityLauncher, - (event as PrivateDmEvent).talkingWith(accountViewModel.userProfile().pubkeyHex) + (event as PrivateDmEvent).talkingWith(accountViewModel.userProfile().pubkeyHex), + event.id ) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt index 1366f3ae7..c93c7993f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -452,7 +452,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi val bookmarks = accountViewModel.userProfile().latestBookmarkList AmberUtils.decrypt( bookmarks?.content ?: "", - accountViewModel.account.keyPair.pubKey.toHexKey() + accountViewModel.account.keyPair.pubKey.toHexKey(), + bookmarks?.id ?: "" ) bookmarks?.decryptedContent = AmberUtils.content AmberUtils.content = "" @@ -474,7 +475,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi val bookmarks = accountViewModel.userProfile().latestBookmarkList AmberUtils.decrypt( bookmarks?.content ?: "", - accountViewModel.account.keyPair.pubKey.toHexKey() + accountViewModel.account.keyPair.pubKey.toHexKey(), + bookmarks?.id ?: "" ) bookmarks?.decryptedContent = AmberUtils.content AmberUtils.content = "" @@ -497,7 +499,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi val bookmarks = accountViewModel.userProfile().latestBookmarkList AmberUtils.decrypt( bookmarks?.content ?: "", - accountViewModel.account.keyPair.pubKey.toHexKey() + accountViewModel.account.keyPair.pubKey.toHexKey(), + bookmarks?.id ?: "" ) bookmarks?.decryptedContent = AmberUtils.content AmberUtils.content = "" @@ -522,7 +525,8 @@ fun NoteDropDownMenu(note: Note, popupExpanded: MutableState, accountVi val bookmarks = accountViewModel.userProfile().latestBookmarkList AmberUtils.decrypt( bookmarks?.content ?: "", - accountViewModel.account.keyPair.pubKey.toHexKey() + accountViewModel.account.keyPair.pubKey.toHexKey(), + bookmarks?.id ?: "" ) bookmarks?.decryptedContent = AmberUtils.content AmberUtils.content = "" 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 7e24f7980..6b51062c5 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 @@ -296,7 +296,6 @@ fun ChatroomScreen( PrivateMessageEditFieldRow(newPostModel, isPrivate = true, accountViewModel) { scope.launch(Dispatchers.IO) { if (newPostModel.nip24 || room.users.size > 1 || replyTo.value?.event is ChatMessageEvent) { - // TODO: add support for amber accountViewModel.account.sendNIP24PrivateMessage( message = newPostModel.message.text, toUsers = room.users.toList(), @@ -639,7 +638,6 @@ fun NewSubjectView(onClose: () -> Unit, accountViewModel: AccountViewModel, room PostButton( onPost = { scope.launch(Dispatchers.IO) { - // TODO: add support for amber accountViewModel.account.sendNIP24PrivateMessage( message = message.value, toUsers = room.users.toList(), diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt index b41c60429..a96eb0be1 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ChatMessageEvent.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Stable import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.crypto.CryptoUtils +import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.encoders.HexKey import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableSet @@ -61,7 +62,7 @@ class ChatMessageEvent( markAsSensitive: Boolean = false, zapRaiserAmount: Long? = null, geohash: String? = null, - privateKey: ByteArray, + keyPair: KeyPair, createdAt: Long = TimeUtils.now() ): ChatMessageEvent { val content = msg @@ -91,10 +92,17 @@ class ChatMessageEvent( tags.add(listOf("subject", it)) } - val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + val pubKey = keyPair.pubKey.toHexKey() val id = generateId(pubKey, createdAt, ClassifiedsEvent.kind, tags, content) - val sig = CryptoUtils.sign(id, privateKey) - return ChatMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) + val sig = if (keyPair.privKey == null) null else CryptoUtils.sign(id, keyPair.privKey) + return ChatMessageEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig?.toHexKey() ?: "") + } + + fun create( + unsignedEvent: ChatMessageEvent, + signature: String + ): ChatMessageEvent { + return ChatMessageEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt index 7fd91f1d5..3946c7f9a 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GiftWrapEvent.kt @@ -36,6 +36,19 @@ class GiftWrapEvent( return myInnerEvent } + fun cachedGift(pubKey: ByteArray, decryptedContent: String): Event? { + val hex = pubKey.toHexKey() + if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex] + + val myInnerEvent = unwrap(decryptedContent) + if (myInnerEvent is WrappedEvent) { + myInnerEvent.host = this + } + + cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent) + return myInnerEvent + } + fun unwrap(privKey: ByteArray) = try { plainContent(privKey)?.let { fromJson(it) } } catch (e: Exception) { @@ -43,6 +56,13 @@ class GiftWrapEvent( null } + fun unwrap(decryptedContent: String) = try { + plainContent(decryptedContent)?.let { fromJson(it) } + } catch (e: Exception) { + // Log.e("UnwrapError", "Couldn't Decrypt the content", e) + null + } + private fun plainContent(privKey: ByteArray): String? { if (content.isEmpty()) return null @@ -60,6 +80,11 @@ class GiftWrapEvent( } } + private fun plainContent(decryptedContent: String): String? { + if (decryptedContent.isEmpty()) return null + return decryptedContent + } + fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1) companion object { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt index d91e8bc40..97f020a20 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/NIP24Factory.kt @@ -9,7 +9,7 @@ class NIP24Factory { fun createMsgNIP24( msg: String, to: List, - from: ByteArray, + keyPair: KeyPair, subject: String? = null, replyTos: List? = null, mentions: List? = null, @@ -18,12 +18,12 @@ class NIP24Factory { zapRaiserAmount: Long? = null, geohash: String? = null ): List { - val senderPublicKey = CryptoUtils.pubkeyCreate(from).toHexKey() + val senderPublicKey = keyPair.pubKey.toHexKey() val senderMessage = ChatMessageEvent.create( msg = msg, to = to, - privateKey = from, + keyPair = keyPair, subject = subject, replyTos = replyTos, mentions = mentions, @@ -38,7 +38,7 @@ class NIP24Factory { event = SealedGossipEvent.create( event = senderMessage, encryptTo = it, - privateKey = from + privateKey = keyPair.privKey!! ), recipientPubKey = it ) @@ -46,7 +46,7 @@ class NIP24Factory { } fun createReactionWithinGroup(content: String, originalNote: EventInterface, to: List, from: KeyPair): List { - val senderPublicKey = CryptoUtils.pubkeyCreate(from.privKey!!).toHexKey() + val senderPublicKey = from.pubKey.toHexKey() val senderReaction = ReactionEvent.create( content, @@ -59,7 +59,7 @@ class NIP24Factory { event = SealedGossipEvent.create( event = senderReaction, encryptTo = it, - privateKey = from.privKey + privateKey = from.privKey!! ), recipientPubKey = it ) @@ -67,7 +67,7 @@ class NIP24Factory { } fun createReactionWithinGroup(emojiUrl: EmojiUrl, originalNote: EventInterface, to: List, from: KeyPair): List { - val senderPublicKey = CryptoUtils.pubkeyCreate(from.privKey!!).toHexKey() + val senderPublicKey = from.pubKey.toHexKey() val senderReaction = ReactionEvent.create( emojiUrl, @@ -80,7 +80,7 @@ class NIP24Factory { event = SealedGossipEvent.create( event = senderReaction, encryptTo = it, - privateKey = from.privKey + privateKey = from.privKey!! ), recipientPubKey = it ) diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt index 2eaa23460..2e3e38fee 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/SealedGossipEvent.kt @@ -38,6 +38,20 @@ class SealedGossipEvent( return event } + fun cachedGossip(pubKey: ByteArray, decryptedContent: String): Event? { + val hex = pubKey.toHexKey() + if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex] + + val gossip = unseal(decryptedContent) + val event = gossip?.mergeWith(this) + if (event is WrappedEvent) { + event.host = host ?: this + } + + cachedInnerEvent = cachedInnerEvent + Pair(hex, event) + return event + } + fun unseal(privKey: ByteArray): Gossip? = try { plainContent(privKey)?.let { Gossip.fromJson(it) } } catch (e: Exception) { @@ -45,6 +59,18 @@ class SealedGossipEvent( null } + fun unseal(decryptedContent: String): Gossip? = try { + plainContent(decryptedContent)?.let { Gossip.fromJson(it) } + } catch (e: Exception) { + Log.w("GossipEvent", "Fail to decrypt or parse Gossip", e) + null + } + + private fun plainContent(decryptedContent: String): String? { + if (decryptedContent.isEmpty()) return null + return decryptedContent + } + private fun plainContent(privKey: ByteArray): String? { if (content.isEmpty()) return null @@ -95,6 +121,23 @@ class SealedGossipEvent( val sig = CryptoUtils.sign(id, privateKey) return SealedGossipEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) } + + fun create( + encryptedContent: String, + pubKey: HexKey, + createdAt: Long = TimeUtils.randomWithinAWeek() + ): SealedGossipEvent { + val tags = listOf>() + val id = generateId(pubKey, createdAt, kind, tags, encryptedContent) + return SealedGossipEvent(id.toHexKey(), pubKey, createdAt, tags, encryptedContent, "") + } + + fun create( + unsignedEvent: SealedGossipEvent, + signature: String + ): SealedGossipEvent { + return SealedGossipEvent(unsignedEvent.id, unsignedEvent.pubKey, unsignedEvent.createdAt, unsignedEvent.tags, unsignedEvent.content, signature) + } } }