diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt index 0ea039b70..e78ac2beb 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/Event.kt @@ -33,7 +33,6 @@ import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.ser.std.StdSerializer -import com.fasterxml.jackson.module.kotlin.addDeserializer import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.encoders.ATag @@ -50,6 +49,8 @@ import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.pointerSizeInBytes +import com.vitorpamplona.quartz.utils.remove +import com.vitorpamplona.quartz.utils.startsWith import java.math.BigDecimal import java.security.MessageDigest @@ -295,6 +296,8 @@ open class Event( null } + fun filterTags(startsWith: Array) = tags.remove(startsWith) + open fun toNIP19(): String = if (this is AddressableEvent) { ATag(kind, pubKey, dTag(), null).toNAddr() diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt index d27221426..b6e0adcfa 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/GeneralListEvent.kt @@ -20,14 +20,10 @@ */ package com.vitorpamplona.quartz.events -import android.util.Log import androidx.compose.runtime.Immutable -import com.fasterxml.jackson.module.kotlin.readValue import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.signers.NostrSigner -import com.vitorpamplona.quartz.utils.bytesUsedInMemory -import com.vitorpamplona.quartz.utils.pointerSizeInBytes import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.toImmutableSet import java.util.HashSet @@ -41,15 +37,7 @@ abstract class GeneralListEvent( tags: Array>, content: String, sig: HexKey, -) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { - @Transient private var privateTagsCache: Array>? = null - - override fun countMemory(): Long = - super.countMemory() + - pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0) - - override fun isContentEncoded() = true - +) : PrivateTagArrayEvent(id, pubKey, createdAt, kind, tags, content, sig) { fun category() = dTag() fun bookmarkedPosts() = taggedEvents() @@ -62,8 +50,6 @@ abstract class GeneralListEvent( fun nameOrTitle() = name()?.ifBlank { null } ?: title()?.ifBlank { null } - fun cachedPrivateTags(): Array>? = privateTagsCache - fun filterTagList( key: String, privateTags: Array>?, @@ -93,30 +79,6 @@ abstract class GeneralListEvent( onReady(isTagged(key, tag)) } - fun privateTags( - signer: NostrSigner, - onReady: (Array>) -> Unit, - ) { - if (content.isEmpty()) { - onReady(emptyArray()) - return - } - - privateTagsCache?.let { - onReady(it) - return - } - - try { - signer.decrypt(content, pubKey) { - privateTagsCache = mapper.readValue>>(it) - privateTagsCache?.let { onReady(it) } - } - } catch (e: Throwable) { - Log.w("GeneralList", "Error parsing the JSON ${e.message}") - } - } - fun privateTagsOrEmpty( signer: NostrSigner, onReady: (Array>) -> Unit, diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateTagArrayEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateTagArrayEvent.kt new file mode 100644 index 000000000..37532e39a --- /dev/null +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/PrivateTagArrayEvent.kt @@ -0,0 +1,271 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import android.util.Log +import androidx.compose.runtime.Immutable +import com.fasterxml.jackson.module.kotlin.readValue +import com.vitorpamplona.quartz.encoders.HexKey +import com.vitorpamplona.quartz.signers.NostrSigner +import com.vitorpamplona.quartz.utils.bytesUsedInMemory +import com.vitorpamplona.quartz.utils.pointerSizeInBytes +import com.vitorpamplona.quartz.utils.remove +import com.vitorpamplona.quartz.utils.replaceAll + +@Immutable +abstract class PrivateTagArrayEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + kind: Int, + tags: Array>, + content: String, + sig: HexKey, +) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { + @Transient private var privateTagsCache: Array>? = null + + override fun countMemory(): Long = + super.countMemory() + + pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0) + + override fun isContentEncoded() = true + + fun cachedPrivateTags(): Array>? = privateTagsCache + + fun privateTags( + signer: NostrSigner, + onReady: (Array>) -> Unit, + ) { + if (content.isEmpty()) { + onReady(emptyArray()) + return + } + + privateTagsCache?.let { + onReady(it) + return + } + + try { + signer.decrypt(content, pubKey) { + privateTagsCache = mapper.readValue>>(it) + privateTagsCache?.let { onReady(it) } + } + } catch (e: Throwable) { + Log.w("GeneralList", "Error parsing the JSON ${e.message}") + } + } + + fun decryptChangeEncrypt( + signer: NostrSigner, + change: (Array>) -> Array>, + onReady: (content: String) -> Unit, + ) { + privateTags(signer) { privateTags -> + encryptTags( + privateTags = change(privateTags), + signer = signer, + ) { encryptedTags -> + onReady(encryptedTags) + } + } + } + + companion object { + fun add( + current: PrivateTagArrayEvent, + newTag: Array, + toPrivate: Boolean, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) { + if (toPrivate) { + current.privateTags(signer) { privateTags -> + encryptTags( + privateTags = privateTags.plus(newTag), + signer = signer, + ) { encryptedTags -> + onReady(encryptedTags, current.tags) + } + } + } else { + onReady(current.content, current.tags.plus(newTag)) + } + } + + fun addAll( + current: PrivateTagArrayEvent, + newTag: Array>, + toPrivate: Boolean, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) { + if (toPrivate) { + current.privateTags(signer) { privateTags -> + encryptTags( + privateTags = privateTags.plus(newTag), + signer = signer, + ) { encryptedTags -> + onReady(encryptedTags, current.tags) + } + } + } else { + onReady(current.content, current.tags.plus(newTag)) + } + } + + fun replaceAllToPrivateNewTag( + dTag: String, + current: PrivateTagArrayEvent?, + oldTagStartsWith: Array, + newTag: Array, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) { + if (current == null) { + createPrivate(dTag, newTag, signer, onReady) + } else { + replaceAllToPrivateNewTag(current, oldTagStartsWith, newTag, signer, onReady) + } + } + + fun replaceAllToPublicNewTag( + dTag: String, + current: PrivateTagArrayEvent?, + oldTagStartsWith: Array, + newTag: Array, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) { + if (current == null) { + createPublic(dTag, newTag, signer, onReady) + } else { + replaceAllToPublicNewTag(current, oldTagStartsWith, newTag, signer, onReady) + } + } + + fun replaceAllToPrivateNewTag( + current: PrivateTagArrayEvent, + oldTagStartsWith: Array, + newTag: Array, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) { + current.privateTags(signer) { privateTags -> + encryptTags( + privateTags = privateTags.replaceAll(oldTagStartsWith, newTag), + signer = signer, + ) { encryptedTags -> + onReady(encryptedTags, current.tags.remove(oldTagStartsWith)) + } + } + } + + fun replaceAllToPublicNewTag( + current: PrivateTagArrayEvent, + oldTagStartsWith: Array, + newTag: Array, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) { + current.privateTags(signer) { privateTags -> + encryptTags( + privateTags = privateTags.remove(oldTagStartsWith), + signer = signer, + ) { encryptedTags -> + onReady(encryptedTags, current.tags.remove(oldTagStartsWith).plus(newTag)) + } + } + } + + fun removeAllFromPrivate( + current: PrivateTagArrayEvent, + oldTagStartsWith: Array, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) { + current.privateTags(signer) { privateTags -> + encryptTags( + privateTags = privateTags.remove(oldTagStartsWith), + signer = signer, + ) { encryptedTags -> + onReady(encryptedTags, current.tags) + } + } + } + + fun removeAllFromPublic( + current: PrivateTagArrayEvent, + oldTagStartsWith: Array, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) = onReady(current.content, current.tags.remove(oldTagStartsWith)) + + fun removeAll( + current: PrivateTagArrayEvent, + oldTagStartsWith: Array, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) { + current.privateTags(signer) { privateTags -> + encryptTags( + privateTags = privateTags.remove(oldTagStartsWith), + signer = signer, + ) { encryptedTags -> + onReady(encryptedTags, current.tags.remove(oldTagStartsWith)) + } + } + } + + fun createPrivate( + dTag: String, + newTag: Array, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) { + encryptTags( + privateTags = arrayOf(newTag), + signer = signer, + ) { encryptedTags -> + onReady(encryptedTags, arrayOf(arrayOf("d", dTag))) + } + } + + fun createPublic( + dTag: String, + newTag: Array, + signer: NostrSigner, + onReady: (content: String, tags: Array>) -> Unit, + ) { + onReady("", arrayOf(arrayOf("d", dTag), newTag)) + } + + fun encryptTags( + privateTags: Array>? = null, + signer: NostrSigner, + onReady: (String) -> Unit, + ) = signer.nip04Encrypt( + if (privateTags.isNullOrEmpty()) "" else mapper.writeValueAsString(privateTags), + signer.pubKey, + onReady, + ) + } +} diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt index 9ef3d5a6e..3def1d85c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/utils/ArrayUtils.kt @@ -29,3 +29,22 @@ public fun removeTrailingNullsAndEmptyOthers(vararg elements: String?): Array.startsWith(startsWith: Array): Boolean { + if (startsWith.size > this.size) return false + for (tagIdx in startsWith.indices) { + if (startsWith[tagIdx] != this[tagIdx]) return false + } + return true +} + +inline fun Array>.filterToArray(predicate: (Array) -> Boolean): Array> = filterTo(ArrayList(), predicate).toTypedArray() + +inline fun Array>.remove(predicate: (Array) -> Boolean): Array> = filterNotTo(ArrayList(this.size), predicate).toTypedArray() + +inline fun Array>.remove(startsWith: Array): Array> = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray() + +inline fun Array>.replaceAll( + startsWith: Array, + newElement: Array, +): Array> = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).plusElement(newElement).toTypedArray()