Separates event class that manages general lists and abstracts another intermediary class that manages private tag arrays in its content.

This commit is contained in:
Vitor Pamplona
2024-12-05 17:55:10 -05:00
parent 8a6f830e29
commit 0dd306b7fa
4 changed files with 295 additions and 40 deletions
@@ -33,7 +33,6 @@ import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.databind.ser.std.StdSerializer
import com.fasterxml.jackson.module.kotlin.addDeserializer
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag 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.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes import com.vitorpamplona.quartz.utils.pointerSizeInBytes
import com.vitorpamplona.quartz.utils.remove
import com.vitorpamplona.quartz.utils.startsWith
import java.math.BigDecimal import java.math.BigDecimal
import java.security.MessageDigest import java.security.MessageDigest
@@ -295,6 +296,8 @@ open class Event(
null null
} }
fun filterTags(startsWith: Array<String>) = tags.remove(startsWith)
open fun toNIP19(): String = open fun toNIP19(): String =
if (this is AddressableEvent) { if (this is AddressableEvent) {
ATag(kind, pubKey, dTag(), null).toNAddr() ATag(kind, pubKey, dTag(), null).toNAddr()
@@ -20,14 +20,10 @@
*/ */
package com.vitorpamplona.quartz.events package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.encoders.ATag import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.signers.NostrSigner 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.ImmutableSet
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import java.util.HashSet import java.util.HashSet
@@ -41,15 +37,7 @@ abstract class GeneralListEvent(
tags: Array<Array<String>>, tags: Array<Array<String>>,
content: String, content: String,
sig: HexKey, sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) { ) : PrivateTagArrayEvent(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient private var privateTagsCache: Array<Array<String>>? = null
override fun countMemory(): Long =
super.countMemory() +
pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0)
override fun isContentEncoded() = true
fun category() = dTag() fun category() = dTag()
fun bookmarkedPosts() = taggedEvents() fun bookmarkedPosts() = taggedEvents()
@@ -62,8 +50,6 @@ abstract class GeneralListEvent(
fun nameOrTitle() = name()?.ifBlank { null } ?: title()?.ifBlank { null } fun nameOrTitle() = name()?.ifBlank { null } ?: title()?.ifBlank { null }
fun cachedPrivateTags(): Array<Array<String>>? = privateTagsCache
fun filterTagList( fun filterTagList(
key: String, key: String,
privateTags: Array<Array<String>>?, privateTags: Array<Array<String>>?,
@@ -93,30 +79,6 @@ abstract class GeneralListEvent(
onReady(isTagged(key, tag)) onReady(isTagged(key, tag))
} }
fun privateTags(
signer: NostrSigner,
onReady: (Array<Array<String>>) -> Unit,
) {
if (content.isEmpty()) {
onReady(emptyArray())
return
}
privateTagsCache?.let {
onReady(it)
return
}
try {
signer.decrypt(content, pubKey) {
privateTagsCache = mapper.readValue<Array<Array<String>>>(it)
privateTagsCache?.let { onReady(it) }
}
} catch (e: Throwable) {
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
}
}
fun privateTagsOrEmpty( fun privateTagsOrEmpty(
signer: NostrSigner, signer: NostrSigner,
onReady: (Array<Array<String>>) -> Unit, onReady: (Array<Array<String>>) -> Unit,
@@ -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<Array<String>>,
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
@Transient private var privateTagsCache: Array<Array<String>>? = null
override fun countMemory(): Long =
super.countMemory() +
pointerSizeInBytes + (privateTagsCache?.sumOf { pointerSizeInBytes + it.sumOf { pointerSizeInBytes + it.bytesUsedInMemory() } } ?: 0)
override fun isContentEncoded() = true
fun cachedPrivateTags(): Array<Array<String>>? = privateTagsCache
fun privateTags(
signer: NostrSigner,
onReady: (Array<Array<String>>) -> Unit,
) {
if (content.isEmpty()) {
onReady(emptyArray())
return
}
privateTagsCache?.let {
onReady(it)
return
}
try {
signer.decrypt(content, pubKey) {
privateTagsCache = mapper.readValue<Array<Array<String>>>(it)
privateTagsCache?.let { onReady(it) }
}
} catch (e: Throwable) {
Log.w("GeneralList", "Error parsing the JSON ${e.message}")
}
}
fun decryptChangeEncrypt(
signer: NostrSigner,
change: (Array<Array<String>>) -> Array<Array<String>>,
onReady: (content: String) -> Unit,
) {
privateTags(signer) { privateTags ->
encryptTags(
privateTags = change(privateTags),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags)
}
}
}
companion object {
fun add(
current: PrivateTagArrayEvent,
newTag: Array<String>,
toPrivate: Boolean,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> 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<Array<String>>,
toPrivate: Boolean,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> 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<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (current == null) {
createPrivate(dTag, newTag, signer, onReady)
} else {
replaceAllToPrivateNewTag(current, oldTagStartsWith, newTag, signer, onReady)
}
}
fun replaceAllToPublicNewTag(
dTag: String,
current: PrivateTagArrayEvent?,
oldTagStartsWith: Array<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
if (current == null) {
createPublic(dTag, newTag, signer, onReady)
} else {
replaceAllToPublicNewTag(current, oldTagStartsWith, newTag, signer, onReady)
}
}
fun replaceAllToPrivateNewTag(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> 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<String>,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> 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<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
current.privateTags(signer) { privateTags ->
encryptTags(
privateTags = privateTags.remove(oldTagStartsWith),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, current.tags)
}
}
}
fun removeAllFromPublic(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) = onReady(current.content, current.tags.remove(oldTagStartsWith))
fun removeAll(
current: PrivateTagArrayEvent,
oldTagStartsWith: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> 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<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
encryptTags(
privateTags = arrayOf(newTag),
signer = signer,
) { encryptedTags ->
onReady(encryptedTags, arrayOf(arrayOf("d", dTag)))
}
}
fun createPublic(
dTag: String,
newTag: Array<String>,
signer: NostrSigner,
onReady: (content: String, tags: Array<Array<String>>) -> Unit,
) {
onReady("", arrayOf(arrayOf("d", dTag), newTag))
}
fun encryptTags(
privateTags: Array<Array<String>>? = null,
signer: NostrSigner,
onReady: (String) -> Unit,
) = signer.nip04Encrypt(
if (privateTags.isNullOrEmpty()) "" else mapper.writeValueAsString(privateTags),
signer.pubKey,
onReady,
)
}
}
@@ -29,3 +29,22 @@ public fun removeTrailingNullsAndEmptyOthers(vararg elements: String?): Array<St
elements[index] ?: "" elements[index] ?: ""
} }
} }
fun Array<String>.startsWith(startsWith: Array<String>): 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<Array<String>>.filterToArray(predicate: (Array<String>) -> Boolean): Array<Array<String>> = filterTo(ArrayList(), predicate).toTypedArray()
inline fun Array<Array<String>>.remove(predicate: (Array<String>) -> Boolean): Array<Array<String>> = filterNotTo(ArrayList(this.size), predicate).toTypedArray()
inline fun Array<Array<String>>.remove(startsWith: Array<String>): Array<Array<String>> = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).toTypedArray()
inline fun Array<Array<String>>.replaceAll(
startsWith: Array<String>,
newElement: Array<String>,
): Array<Array<String>> = filterNotTo(ArrayList(this.size), { it.startsWith(startsWith) }).plusElement(newElement).toTypedArray()