diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt index 93bcdc372..899495f67 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/privateChats/ChatroomList.kt @@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.utils.LargeCache import kotlinx.collections.immutable.persistentSetOf @@ -42,6 +43,26 @@ class ChatroomList( fun getOrCreatePrivateChatroom(key: ChatroomKey): Chatroom = getOrCreatePrivateChatroomSync(key) + fun add( + event: ChatroomKeyable, + msg: Note, + ) { + if (event.isIncluded(ownerPubKey)) { + val key = event.chatroomKey(ownerPubKey) + addMessage(key, msg) + } + } + + fun delete( + event: ChatroomKeyable, + msg: Note, + ) { + if (event.isIncluded(ownerPubKey)) { + val key = event.chatroomKey(ownerPubKey) + removeMessage(key, msg) + } + } + fun addMessage( room: ChatroomKey, msg: Note, @@ -49,6 +70,9 @@ class ChatroomList( val privateChatroom = getOrCreatePrivateChatroom(room) if (msg !in privateChatroom.messages) { privateChatroom.addMessageSync(msg) + if (msg.author?.pubkeyHex == ownerPubKey) { + privateChatroom.ownerSentMessage = true + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt index 7bca57ce9..3702ca26e 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/DecryptAndIndexProcessor.kt @@ -25,42 +25,64 @@ import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.model.privateChats.ChatroomList import com.vitorpamplona.quartz.experimental.ephemChat.chat.EphemeralChatEvent import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.core.IEvent import com.vitorpamplona.quartz.nip03Timestamp.OtsEvent -import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent -import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey -import com.vitorpamplona.quartz.nip17Dm.files.ChatMessageEncryptedFileHeaderEvent -import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent +import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent import com.vitorpamplona.quartz.nip37Drafts.DraftEvent import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent +import com.vitorpamplona.quartz.nip57Zaps.PrivateZapCache import com.vitorpamplona.quartz.nip59Giftwrap.seals.SealedRumorEvent import com.vitorpamplona.quartz.nip59Giftwrap.wraps.GiftWrapEvent import kotlinx.coroutines.CancellationException -import kotlin.reflect.KClass class EventProcessor( private val account: Account, private val cache: LocalCache, ) { - private val decryptionService = DecryptionService(account) private val indexingService = IndexingService(account, cache) - private val chatroomService = ChatroomService(account) - private val eventHandlers = createEventHandlers() + + private val chatHandler = ChatHandler(account.chatroomList) + private val otsHandler = OtsEventHandler(account) + + private val draftHandler = DraftEventHandler(account, indexingService) + + private val giftWrapHandler = GiftWrapEventHandler(account, cache, this) + private val sealHandler = SealedRumorEventHandler(account, cache, this) + + private val zapRequest = LnZapRequestEventHandler(account.privateZapsDecryptionCache) + private val zapEvent = LnZapEventHandler(account.privateZapsDecryptionCache) suspend fun consume(note: Note) { note.event?.let { event -> try { - processEvent(event, note, note) + consumeEvent(event, note, note) } catch (e: Exception) { Log.e("EventProcessor", "Error processing note", e) } } } + internal suspend fun consumeEvent( + event: Event, + eventNote: Note, + publicNote: Note, + ) { + when (event) { + is ChatroomKeyable -> chatHandler.add(event, eventNote, publicNote) + is OtsEvent -> otsHandler.add(event, eventNote, publicNote) + is DraftEvent -> draftHandler.add(event, eventNote, publicNote) + is GiftWrapEvent -> giftWrapHandler.add(event, eventNote, publicNote) + is SealedRumorEvent -> sealHandler.add(event, eventNote, publicNote) + is LnZapRequestEvent -> zapRequest.add(event, eventNote, publicNote) + } + } + suspend fun delete(note: Note) { note.event?.let { event -> try { @@ -71,34 +93,21 @@ class EventProcessor( } } - internal suspend fun processEvent( - event: Event, - eventNote: Note, - publicNote: Note, - ) { - eventHandlers[event::class]?.process(event, eventNote, publicNote) - } - internal suspend fun deleteEvent( event: Event, - eventNote: Note, + note: Note, ) { - eventHandlers[event::class]?.delete(event, eventNote) + when (event) { + is ChatroomKeyable -> chatHandler.delete(event, note) + is OtsEvent -> otsHandler.delete(event, note) + is DraftEvent -> draftHandler.delete(event, note) + is GiftWrapEvent -> giftWrapHandler.delete(event, note) + is SealedRumorEvent -> sealHandler.delete(event, note) + is LnZapRequestEvent -> zapRequest.delete(event, note) + is LnZapEvent -> zapEvent.delete(event, note) + } } - private fun createEventHandlers(): Map, EventHandler> = - mapOf( - PrivateDmEvent::class to PrivateDmHandler(chatroomService), - ChatMessageEvent::class to ChatMessageHandler(chatroomService), - ChatMessageEncryptedFileHeaderEvent::class to ChatMessageEncryptedFileHeaderHandler(chatroomService), - OtsEvent::class to OtsEventHandler(account), - DraftEvent::class to DraftEventHandler(decryptionService, indexingService), - GiftWrapEvent::class to GiftWrapEventHandler(decryptionService, cache, this), - SealedRumorEvent::class to SealedRumorEventHandler(decryptionService, cache, this), - LnZapRequestEvent::class to LnZapRequestEventHandler(decryptionService), - LnZapEvent::class to LnZapEventHandler(decryptionService), - ) - suspend fun runNew(newNotes: Set) { try { newNotes.forEach { consume(it) } @@ -122,7 +131,14 @@ class EventProcessor( val deletedDrafts = newNotes.mapNotNull { note -> val event = note.event - if (event is DraftEvent && event.isDeleted()) note else null + if (event is DraftEvent && + event.isDeleted() && + !cache.deletionIndex.hasBeenDeleted(event) + ) { + note + } else { + null + } } if (deletedDrafts.isNotEmpty()) { @@ -132,38 +148,6 @@ class EventProcessor( } } -class DecryptionService( - val account: Account, -) { - suspend fun unwrapGiftWrap(event: GiftWrapEvent): Event? = event.unwrapOrNull(account.signer) - - suspend fun unsealRumor(event: SealedRumorEvent): Event? = event.unsealOrNull(account.signer) - - suspend fun decryptDraft(event: DraftEvent): Event? = - account.draftsDecryptionCache.preCachedDraft(event) - ?: account.draftsDecryptionCache.cachedDraft(event) - - suspend fun handlePrivateZap(event: LnZapRequestEvent) { - if (account.privateZapsDecryptionCache.cachedPrivateZap(event) == null && event.isPrivateZap()) { - account.privateZapsDecryptionCache.decryptPrivateZap(event) - } - } - - fun deletePrivateZap(event: LnZapRequestEvent) { - if (event.isPrivateZap()) { - account.privateZapsDecryptionCache.delete(event) - } - } - - fun deletePrivateZapFromZapEvent(event: LnZapEvent) { - event.zapRequest?.let { req -> - if (req.isPrivateZap()) { - account.privateZapsDecryptionCache.delete(req) - } - } - } -} - class IndexingService( private val account: Account, private val cache: LocalCache, @@ -171,44 +155,12 @@ class IndexingService( fun indexDraftAsRealEvent( draftEventWrap: Note, rumor: Event, - ) { - setupReplyRelationships(draftEventWrap, rumor) - indexByEventType(draftEventWrap, rumor) - } - - private fun setupReplyRelationships( - draftEventWrap: Note, - rumor: Event, ) { draftEventWrap.replyTo = cache.computeReplyTo(rumor) draftEventWrap.replyTo?.forEach { it.addReply(draftEventWrap) } - } - - private fun indexByEventType( - draftEventWrap: Note, - rumor: Event, - ) { - val chatroomService = ChatroomService(account) when (rumor) { - is PrivateDmEvent -> { - if (rumor.canDecrypt(account.signer)) { - val key = rumor.chatroomKey(account.signer.pubKey) - chatroomService.addMessage(key, draftEventWrap) - } - } - is ChatMessageEvent -> { - if (rumor.isIncluded(account.signer.pubKey)) { - val key = rumor.chatroomKey(account.signer.pubKey) - chatroomService.addMessage(key, draftEventWrap) - } - } - is ChatMessageEncryptedFileHeaderEvent -> { - if (rumor.isIncluded(account.signer.pubKey)) { - val key = rumor.chatroomKey(account.signer.pubKey) - chatroomService.addMessage(key, draftEventWrap) - } - } + is ChatroomKeyable -> account.chatroomList.add(rumor, draftEventWrap) is EphemeralChatEvent -> { rumor.roomId()?.let { roomId -> val channel = cache.getOrCreateEphemeralChannel(roomId) @@ -231,160 +183,77 @@ class IndexingService( } } -class ChatroomService( - val account: Account, -) { - fun addMessage( - chatroomKey: ChatroomKey, - note: Note, - ) { - account.chatroomList.addMessage(chatroomKey, note) - } - - fun removeMessage( - chatroomKey: ChatroomKey, - note: Note, - ) { - account.chatroomList.removeMessage(chatroomKey, note) - } -} - -interface EventHandler { - suspend fun process( - event: Event, +interface EventHandler { + suspend fun add( + event: T, eventNote: Note, publicNote: Note, ) {} suspend fun delete( - event: Event, + event: T, eventNote: Note, ) {} } -class PrivateDmHandler( - private val chatroomService: ChatroomService, -) : EventHandler { - override suspend fun process( - event: Event, +class ChatHandler( + private val chatroomList: ChatroomList, +) : EventHandler { + override suspend fun add( + event: ChatroomKeyable, eventNote: Note, publicNote: Note, ) { - event as PrivateDmEvent - if (event.canDecrypt(chatroomService.account.signer.pubKey)) { - val key = event.chatroomKey(chatroomService.account.signer.pubKey) - chatroomService.addMessage(key, eventNote) - } + chatroomList.add(event, eventNote) } override suspend fun delete( - event: Event, + event: ChatroomKeyable, eventNote: Note, ) { - event as PrivateDmEvent - if (event.canDecrypt(chatroomService.account.signer.pubKey)) { - val key = event.chatroomKey(chatroomService.account.signer.pubKey) - chatroomService.removeMessage(key, eventNote) - } - } -} - -class ChatMessageHandler( - private val chatroomService: ChatroomService, -) : EventHandler { - override suspend fun process( - event: Event, - eventNote: Note, - publicNote: Note, - ) { - event as ChatMessageEvent - if (event.isIncluded(chatroomService.account.signer.pubKey)) { - val key = event.chatroomKey(chatroomService.account.signer.pubKey) - chatroomService.addMessage(key, eventNote) - } - } - - override suspend fun delete( - event: Event, - eventNote: Note, - ) { - event as ChatMessageEvent - if (event.isIncluded(chatroomService.account.signer.pubKey)) { - val key = event.chatroomKey(chatroomService.account.signer.pubKey) - chatroomService.removeMessage(key, eventNote) - } - } -} - -class ChatMessageEncryptedFileHeaderHandler( - private val chatroomService: ChatroomService, -) : EventHandler { - override suspend fun process( - event: Event, - eventNote: Note, - publicNote: Note, - ) { - event as ChatMessageEncryptedFileHeaderEvent - if (event.isIncluded(chatroomService.account.signer.pubKey)) { - val key = event.chatroomKey(chatroomService.account.signer.pubKey) - chatroomService.addMessage(key, eventNote) - } - } - - override suspend fun delete( - event: Event, - eventNote: Note, - ) { - event as ChatMessageEncryptedFileHeaderEvent - if (event.isIncluded(chatroomService.account.signer.pubKey)) { - val key = event.chatroomKey(chatroomService.account.signer.pubKey) - chatroomService.removeMessage(key, eventNote) - } + chatroomList.delete(event, eventNote) } } class OtsEventHandler( private val account: Account, -) : EventHandler { - override suspend fun process( - event: Event, +) : EventHandler { + override suspend fun add( + event: OtsEvent, eventNote: Note, publicNote: Note, ) { - event as OtsEvent Amethyst.instance.otsVerifCache.cacheVerify(event, account.otsResolverBuilder) } } class DraftEventHandler( - private val decryptionService: DecryptionService, + private val account: Account, private val indexingService: IndexingService, -) : EventHandler { - override suspend fun process( - event: Event, +) : EventHandler { + override suspend fun add( + event: DraftEvent, eventNote: Note, publicNote: Note, ) { - event as DraftEvent - if (event.pubKey == decryptionService.account.signer.pubKey && !event.isDeleted()) { - val rumor = decryptionService.decryptDraft(event) + if (event.pubKey == account.signer.pubKey && !event.isDeleted()) { + val rumor = account.draftsDecryptionCache.preCachedDraft(event) ?: account.draftsDecryptionCache.cachedDraft(event) rumor?.let { indexingService.indexDraftAsRealEvent(eventNote, it) } } } } class GiftWrapEventHandler( - private val decryptionService: DecryptionService, + private val account: Account, private val cache: LocalCache, private val eventProcessor: EventProcessor, -) : EventHandler { - override suspend fun process( - event: Event, +) : EventHandler { + override suspend fun add( + event: GiftWrapEvent, eventNote: Note, publicNote: Note, ) { - event as GiftWrapEvent - if (event.recipientPubKey() != decryptionService.account.signer.pubKey) return + if (event.recipientPubKey() != account.signer.pubKey) return val innerGiftId = event.innerEventId if (innerGiftId == null) { @@ -395,11 +264,10 @@ class GiftWrapEventHandler( } override suspend fun delete( - event: Event, + event: GiftWrapEvent, eventNote: Note, ) { - event as GiftWrapEvent - if (event.recipientPubKey() != decryptionService.account.signer.pubKey) return + if (event.recipientPubKey() != account.signer.pubKey) return event.innerEventId?.let { innerGiftId -> val innerGiftNote = cache.getNoteIfExists(innerGiftId) @@ -414,13 +282,13 @@ class GiftWrapEventHandler( eventNote: Note, publicNote: Note, ) { - val innerGift = decryptionService.unwrapGiftWrap(event) ?: return + val innerGift = event.unwrapOrNull(account.signer) ?: return eventNote.event = event.copyNoContent() if (cache.justConsume(innerGift, null, false)) { cache.copyRelaysFromTo(publicNote, innerGift) val innerGiftNote = cache.getOrCreateNote(innerGift.id) - eventProcessor.processEvent(innerGift, innerGiftNote, publicNote) + eventProcessor.consumeEvent(innerGift, innerGiftNote, publicNote) } } @@ -431,23 +299,21 @@ class GiftWrapEventHandler( cache.copyRelaysFromTo(publicNote, innerGiftId) val innerGiftNote = cache.getOrCreateNote(innerGiftId) innerGiftNote.event?.let { innerGift -> - eventProcessor.processEvent(innerGift, innerGiftNote, publicNote) + eventProcessor.consumeEvent(innerGift, innerGiftNote, publicNote) } } } class SealedRumorEventHandler( - private val decryptionService: DecryptionService, + private val account: Account, private val cache: LocalCache, private val eventProcessor: EventProcessor, -) : EventHandler { - override suspend fun process( - event: Event, +) : EventHandler { + override suspend fun add( + event: SealedRumorEvent, eventNote: Note, publicNote: Note, ) { - event as SealedRumorEvent - val rumorId = event.innerEventId if (rumorId == null) { processNewSealedRumor(event, eventNote, publicNote) @@ -457,11 +323,9 @@ class SealedRumorEventHandler( } override suspend fun delete( - event: Event, + event: SealedRumorEvent, eventNote: Note, ) { - event as SealedRumorEvent - event.innerEventId?.let { rumorId -> val innerRumorNote = cache.getNoteIfExists(rumorId) innerRumorNote?.event?.let { innerRumor -> @@ -475,14 +339,14 @@ class SealedRumorEventHandler( eventNote: Note, publicNote: Note, ) { - val innerRumor = decryptionService.unsealRumor(event) ?: return + val innerRumor = event.unsealOrNull(account.signer) ?: return eventNote.event = event.copyNoContent() cache.justConsume(innerRumor, null, true) cache.copyRelaysFromTo(publicNote, innerRumor) val innerRumorNote = cache.getOrCreateNote(innerRumor.id) - eventProcessor.processEvent(innerRumor, innerRumorNote, publicNote) + eventProcessor.consumeEvent(innerRumor, innerRumorNote, publicNote) } private suspend fun processExistingSealedRumor( @@ -492,40 +356,45 @@ class SealedRumorEventHandler( cache.copyRelaysFromTo(publicNote, rumorId) val innerRumorNote = cache.getOrCreateNote(rumorId) innerRumorNote.event?.let { innerRumor -> - eventProcessor.processEvent(innerRumor, innerRumorNote, publicNote) + eventProcessor.consumeEvent(innerRumor, innerRumorNote, publicNote) } } } class LnZapRequestEventHandler( - private val decryptionService: DecryptionService, -) : EventHandler { - override suspend fun process( - event: Event, + val decryptionCache: PrivateZapCache, +) : EventHandler { + override suspend fun add( + event: LnZapRequestEvent, eventNote: Note, publicNote: Note, ) { - event as LnZapRequestEvent - decryptionService.handlePrivateZap(event) + if (decryptionCache.cachedPrivateZap(event) == null && event.isPrivateZap()) { + decryptionCache.decryptPrivateZap(event) + } } override suspend fun delete( - event: Event, + event: LnZapRequestEvent, eventNote: Note, ) { - event as LnZapRequestEvent - decryptionService.deletePrivateZap(event) + if (event.isPrivateZap()) { + decryptionCache.delete(event) + } } } class LnZapEventHandler( - private val decryptionService: DecryptionService, -) : EventHandler { + val decryptionCache: PrivateZapCache, +) : EventHandler { override suspend fun delete( - event: Event, + event: LnZapEvent, eventNote: Note, ) { - event as LnZapEvent - decryptionService.deletePrivateZapFromZapEvent(event) + event.zapRequest?.let { req -> + if (req.isPrivateZap()) { + decryptionCache.delete(req) + } + } } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt index 0ae190332..d1822588b 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/PrivateDMCache.kt @@ -31,7 +31,7 @@ class PrivateDMCache( private val decryptionCache = object : LruCache(10000) { override fun create(key: PrivateDmEvent): PrivateDMDecryptCache? { - val canDecrypt = key.canDecrypt(signer.pubKey) + val canDecrypt = key.isIncluded(signer.pubKey) return if (key.content.isNotBlank() && canDecrypt) { PrivateDMDecryptCache(signer) } else { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt index 68d68a805..7b7b99979 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip04Dm/messages/PrivateDmEvent.kt @@ -58,12 +58,12 @@ class PrivateDmEvent( override fun isContentEncoded() = true - fun canDecrypt(signer: NostrSigner) = canDecrypt(signer.pubKey) + fun isIncluded(signer: NostrSigner) = isIncluded(signer.pubKey) - fun canDecrypt(signerPubKey: HexKey) = pubKey == signerPubKey || recipientPubKey() == signerPubKey + override fun isIncluded(user: HexKey) = pubKey == user || recipientPubKey() == user suspend fun decryptContent(signer: NostrSigner): String { - if (!canDecrypt(signer.pubKey)) throw SignerExceptions.UnauthorizedDecryptionException() + if (!isIncluded(signer.pubKey)) throw SignerExceptions.UnauthorizedDecryptionException() val retVal = signer.decrypt(content, talkingWith(signer.pubKey)) return if (retVal.startsWith(NIP_18_ADVERTISEMENT)) { diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt index b09d7e79c..6000f7f66 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip17Dm/base/ChatroomKeyable.kt @@ -21,7 +21,10 @@ package com.vitorpamplona.quartz.nip17Dm.base import com.vitorpamplona.quartz.nip01Core.core.HexKey +import com.vitorpamplona.quartz.nip01Core.core.IEvent + +interface ChatroomKeyable : IEvent { + fun isIncluded(user: HexKey): Boolean -interface ChatroomKeyable { fun chatroomKey(toRemove: HexKey): ChatroomKey }