Merge branch 'vitorpamplona:main' into labeled-bookmarks

This commit is contained in:
KotlinGeekDev
2025-11-13 09:15:43 +00:00
committed by GitHub
157 changed files with 5269 additions and 1436 deletions
@@ -22,9 +22,11 @@ package com.vitorpamplona.quartz.nip01Core.core
import android.os.Parcel
import android.os.Parcelable
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
@Stable
actual data class Address actual constructor(
actual val kind: Kind,
actual val pubKeyHex: HexKey,
@@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.vitorpamplona.quartz.nip01Core.jackson.InliningTagArrayPrettyPrinter
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResultJsonDeserializer
@@ -50,9 +51,9 @@ object JsonMapperNip55 {
.addSerializer(Permission::class.java, PermissionSerializer()),
)
inline fun <reified T> fromJsonTo(json: String): T = defaultMapper.readValue(json, T::class.java)
inline fun <reified T> fromJsonTo(json: String): T = defaultMapper.readValue<T>(json)
inline fun <reified T> fromJsonTo(json: InputStream): T = defaultMapper.readValue(json, T::class.java)
inline fun <reified T> fromJsonTo(json: InputStream): T = defaultMapper.readValue<T>(json)
fun toJson(event: ArrayNode): String = defaultMapper.writeValueAsString(event)
@@ -26,6 +26,7 @@ import android.net.Uri
import com.vitorpamplona.quartz.nip55AndroidSigner.api.IResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException
fun <T : IResult> ContentResolver.query(
uri: Uri,
@@ -51,6 +52,7 @@ fun <T : IResult> ContentResolver.query(
}
}
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("ExternalSignerLauncher", "Failed to query the Signer app in the background", e)
SignerResult.RequestIncomplete.ErrorExceptionCallingContentResolver(e)
}
@@ -34,7 +34,7 @@ class DecryptZapResponse {
)
fun parse(intent: IntentResult): SignerResult.RequestAddressed<ZapEventDecryptionResult> {
if (intent.rejected) {
if (intent.rejected == true) {
return SignerResult.RequestAddressed.ManuallyRejected()
}
val eventJson = intent.result
@@ -33,7 +33,7 @@ class DeriveKeyResponse {
)
fun parse(intent: IntentResult): SignerResult.RequestAddressed<DerivationResult> {
if (intent.rejected) {
if (intent.rejected == true) {
return SignerResult.RequestAddressed.ManuallyRejected()
}
val newPrivateKey = intent.result
@@ -32,7 +32,7 @@ class Nip04DecryptResponse {
)
fun parse(intent: IntentResult): SignerResult.RequestAddressed<DecryptionResult> {
if (intent.rejected) {
if (intent.rejected == true) {
return SignerResult.RequestAddressed.ManuallyRejected()
}
val plaintext = intent.result
@@ -32,7 +32,7 @@ class Nip04EncryptResponse {
)
fun parse(intent: IntentResult): SignerResult.RequestAddressed<EncryptionResult> {
if (intent.rejected) {
if (intent.rejected == true) {
return SignerResult.RequestAddressed.ManuallyRejected()
}
@@ -32,7 +32,7 @@ class Nip44DecryptResponse {
)
fun parse(intent: IntentResult): SignerResult.RequestAddressed<DecryptionResult> {
if (intent.rejected) {
if (intent.rejected == true) {
return SignerResult.RequestAddressed.ManuallyRejected()
}
val plaintext = intent.result
@@ -32,7 +32,7 @@ class Nip44EncryptResponse {
)
fun parse(intent: IntentResult): SignerResult.RequestAddressed<EncryptionResult> {
if (intent.rejected) {
if (intent.rejected == true) {
return SignerResult.RequestAddressed.ManuallyRejected()
}
val ciphertext = intent.result
@@ -39,7 +39,7 @@ class SignResponse {
intent: IntentResult,
unsignedEvent: Event,
): SignerResult.RequestAddressed<SignResult> {
if (intent.rejected) {
if (intent.rejected == true) {
return SignerResult.RequestAddressed.ManuallyRejected()
}
@@ -31,16 +31,17 @@ data class IntentResult(
val result: String? = null,
val event: String? = null,
val id: String? = null,
val rejected: Boolean = false,
val rejected: Boolean? = false,
) : OptimizedSerializable {
fun toJson(): String = JsonMapperNip55.toJson(this)
fun toIntent(): Intent {
val intent = Intent()
intent.putExtra("id", id)
intent.putExtra("result", result)
intent.putExtra("event", event)
intent.putExtra("package", `package`)
if (id != null) intent.putExtra("id", id)
if (result != null) intent.putExtra("result", result)
if (event != null) intent.putExtra("event", event)
if (`package` != null) intent.putExtra("package", `package`)
if (rejected != null) intent.putExtra("rejected", rejected)
return intent
}
@@ -51,7 +52,12 @@ data class IntentResult(
result = data.getStringExtra("result"),
event = data.getStringExtra("event"),
`package` = data.getStringExtra("package"),
rejected = data.extras?.containsKey("rejected") == true,
rejected =
if (data.extras?.containsKey("rejected") == true) {
data.getBooleanExtra("rejected", false)
} else {
null
},
)
fun fromJson(json: String): IntentResult = JsonMapperNip55.fromJsonTo<IntentResult>(json)
@@ -24,6 +24,8 @@ import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.utils.asBooleanOrNull
import com.vitorpamplona.quartz.utils.asTextOrNull
class IntentResultJsonDeserializer : StdDeserializer<IntentResult>(IntentResult::class.java) {
override fun deserialize(
@@ -31,12 +33,13 @@ class IntentResultJsonDeserializer : StdDeserializer<IntentResult>(IntentResult:
ctxt: DeserializationContext,
): IntentResult {
val jsonObject: JsonNode = jp.codec.readTree(jp)
return IntentResult(
`package` = jsonObject.get("package")?.asText()?.intern(),
result = jsonObject.get("result")?.asText(),
event = jsonObject.get("event")?.asText(),
id = jsonObject.get("id")?.asText()?.intern(),
rejected = jsonObject.get("rejected")?.asBoolean() ?: false,
`package` = jsonObject.get("package")?.asTextOrNull()?.intern(),
result = jsonObject.get("result")?.asTextOrNull(),
event = jsonObject.get("event")?.asTextOrNull(),
id = jsonObject.get("id")?.asTextOrNull()?.intern(),
rejected = jsonObject.get("rejected")?.asBooleanOrNull(),
)
}
}
@@ -35,7 +35,7 @@ class IntentResultJsonSerializer : StdSerializer<IntentResult>(IntentResult::cla
result.result?.let { gen.writeStringField("result", it) }
result.event?.let { gen.writeStringField("event", it) }
result.id?.let { gen.writeStringField("id", it) }
result.rejected.let { gen.writeBooleanField("rejected", it) }
result.rejected?.let { gen.writeBooleanField("rejected", it) }
gen.writeEndObject()
}
}
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2025 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.nip55AndroidSigner.foreground.intents.results
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class IntentResultSerializerTest {
val example =
"""
[
{
"package": null,
"signature": "336590c3f90dc6b3e709090f48b63cc82db01f4a53702d3a9802c7647d43b8a946964fd8178a5439a655e6a2a9af1143573c64d5828f526b2bf9b24bfbde61dd",
"result": "336590c3f90dc6b3e709090f48b63cc82db01f4a53702d3a9802c7647d43b8a946964fd8178a5439a655e6a2a9af1143573c64d5828f526b2bf9b24bfbde61dd",
"rejected": null,
"id": "z6AkVNy2jAH4vcUcUaYIZHrOhi6oWROj"
},
{
"package": null,
"signature": "2560b238cabffd3c4b02b3f9f131fceb96c388f68e4a60382b833b74871ef5474baa409e6278768a47330e8b1be041a3980c242fb05014d451603e86e6240683",
"result": "2560b238cabffd3c4b02b3f9f131fceb96c388f68e4a60382b833b74871ef5474baa409e6278768a47330e8b1be041a3980c242fb05014d451603e86e6240683",
"rejected": null,
"id": "ZQGMSmJbSbqCBRxc7elKskWHEPldIJ2j"
},
{
"package": null,
"signature": "3577ce8638367ed0569e1953bc8379ec252d66903edf6e607bbc0c081039b3ca8bee82df1956981e598a7ea172d84671d66b052a138ce41c5f43f96aed05f536",
"result": "3577ce8638367ed0569e1953bc8379ec252d66903edf6e607bbc0c081039b3ca8bee82df1956981e598a7ea172d84671d66b052a138ce41c5f43f96aed05f536",
"rejected": null,
"id": "yuIGh0hwUN5vN3mYTJoMAP1kE7EjedEu"
},
{
"package": null,
"signature": "2c39187a337083f473e0b3b31867efbc5399e5d64e8085295eb6f5cd94149f7a2563be90a38ca26519b2e4943137d08ebd743ae7c65d11715c9ff8b99d676c57",
"result": "2c39187a337083f473e0b3b31867efbc5399e5d64e8085295eb6f5cd94149f7a2563be90a38ca26519b2e4943137d08ebd743ae7c65d11715c9ff8b99d676c57",
"rejected": null,
"id": "iAGHGc9OKEkSze36Zqhtep0cGi26oWZp"
}
]
""".trimIndent()
@Test
fun testDeserializer() {
val results = IntentResult.fromJsonArray(example)
println("${results.get(0).javaClass.simpleName}")
assertEquals(4, results.size)
assertEquals("z6AkVNy2jAH4vcUcUaYIZHrOhi6oWROj", results[0].id)
assertNull(results[0].`package`)
assertEquals("336590c3f90dc6b3e709090f48b63cc82db01f4a53702d3a9802c7647d43b8a946964fd8178a5439a655e6a2a9af1143573c64d5828f526b2bf9b24bfbde61dd", results[0].result)
assertNull(results[0].rejected)
assertEquals("ZQGMSmJbSbqCBRxc7elKskWHEPldIJ2j", results[1].id)
assertNull(results[1].`package`)
assertEquals("2560b238cabffd3c4b02b3f9f131fceb96c388f68e4a60382b833b74871ef5474baa409e6278768a47330e8b1be041a3980c242fb05014d451603e86e6240683", results[1].result)
assertNull(results[1].rejected)
assertEquals("yuIGh0hwUN5vN3mYTJoMAP1kE7EjedEu", results[2].id)
assertNull(results[2].`package`)
assertEquals("3577ce8638367ed0569e1953bc8379ec252d66903edf6e607bbc0c081039b3ca8bee82df1956981e598a7ea172d84671d66b052a138ce41c5f43f96aed05f536", results[2].result)
assertNull(results[2].rejected)
assertEquals("iAGHGc9OKEkSze36Zqhtep0cGi26oWZp", results[3].id)
assertNull(results[3].`package`)
assertEquals("2c39187a337083f473e0b3b31867efbc5399e5d64e8085295eb6f5cd94149f7a2563be90a38ca26519b2e4943137d08ebd743ae7c65d11715c9ff8b99d676c57", results[3].result)
assertNull(results[3].rejected)
}
}
@@ -60,6 +60,12 @@ class TagArrayBuilder<T : IEvent> {
return this
}
fun addFirst(tag: Array<String>): TagArrayBuilder<T> {
if (tag.isEmpty() || tag[0].isEmpty()) return this
tagList.getOrPut(tag[0], ::mutableListOf).add(0, tag)
return this
}
fun addUnique(tag: Array<String>): TagArrayBuilder<T> {
if (tag.isEmpty() || tag[0].isEmpty()) return this
tagList[tag[0]] = mutableListOf(tag)
@@ -42,22 +42,22 @@ import kotlinx.coroutines.flow.callbackFlow
* - They will be ignored if they are already in the list.
* - They will be added to the beginning of the list if they are new.
*/
fun INostrClient.reqResultsInOrderAsFlow(
fun INostrClient.reqAsFlow(
relay: String,
filters: List<Filter>,
) = reqResultsInOrderAsFlow(RelayUrlNormalizer.normalize(relay), filters)
) = reqAsFlow(RelayUrlNormalizer.normalize(relay), filters)
fun INostrClient.reqResultsInOrderAsFlow(
fun INostrClient.reqAsFlow(
relay: String,
filter: Filter,
) = reqResultsInOrderAsFlow(RelayUrlNormalizer.normalize(relay), listOf(filter))
) = reqAsFlow(RelayUrlNormalizer.normalize(relay), listOf(filter))
fun INostrClient.reqResultsInOrderAsFlow(
fun INostrClient.reqAsFlow(
relay: NormalizedRelayUrl,
filter: Filter,
) = reqResultsInOrderAsFlow(relay, listOf(filter))
) = reqAsFlow(relay, listOf(filter))
fun INostrClient.reqResultsInOrderAsFlow(
fun INostrClient.reqAsFlow(
relay: NormalizedRelayUrl,
filters: List<Filter>,
): Flow<List<Event>> =
@@ -65,7 +65,7 @@ fun INostrClient.reqResultsInOrderAsFlow(
val subId = RandomInstance.randomChars(10)
var hasBeenLive = false
val eventIds = mutableSetOf<HexKey>()
val events = mutableListOf<Event>()
var currentEvents = listOf<Event>()
val listener =
object : IRequestListener {
@@ -77,12 +77,16 @@ fun INostrClient.reqResultsInOrderAsFlow(
) {
if (event.id !in eventIds) {
if (hasBeenLive) {
events.add(0, event)
// faster
val list = ArrayList<Event>(1 + currentEvents.size)
list.add(event)
list.addAll(currentEvents)
currentEvents = list
} else {
events.add(event)
currentEvents = currentEvents + event
}
eventIds.add(event.id)
trySend(events.toList())
trySend(currentEvents)
}
}
@@ -54,3 +54,8 @@ fun <T : Event> eventUpdate(
createdAt: Long = TimeUtils.now(),
updater: TagArrayBuilder<T>.() -> Unit = {},
) = EventTemplate<T>(createdAt, base.kind, base.tags.builder(updater), base.content)
fun <T : Event> T.update(
createdAt: Long = TimeUtils.now(),
updater: TagArrayBuilder<T>.() -> Unit = {},
) = eventUpdate(this, createdAt, updater)
@@ -21,6 +21,7 @@
package com.vitorpamplona.quartz.nip51Lists.followList
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
@@ -70,6 +71,16 @@ class FollowListEvent(
const val KIND = 39089
const val ALT = "List of people to follow"
fun createAddress(
pubKey: HexKey,
dTag: String,
) = Address(KIND, pubKey, dTag)
fun listFor(
pubKey: HexKey,
dTag: String,
): String = Address.assemble(KIND, pubKey, dTag)
@OptIn(ExperimentalUuidApi::class)
suspend fun create(
name: String,
@@ -161,7 +172,7 @@ class FollowListEvent(
) {
dTag(dTag)
alt(ALT)
name(name)
title(name)
people(people)
initializer()
@@ -20,10 +20,32 @@
*/
package com.vitorpamplona.quartz.nip51Lists.followList
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
fun TagArrayBuilder<FollowListEvent>.name(name: String) = addUnique(NameTag.assemble(name))
fun TagArrayBuilder<FollowListEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<FollowListEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc))
fun TagArrayBuilder<FollowListEvent>.image(imageUrl: String) = addUnique(ImageTag.assemble(imageUrl))
fun TagArrayBuilder<FollowListEvent>.people(peoples: List<UserTag>) = addAll(peoples.map { it.toTagArray() })
fun TagArrayBuilder<FollowListEvent>.person(person: UserTag) = add(person.toTagArray())
fun TagArrayBuilder<FollowListEvent>.person(
pubkey: HexKey,
relayHint: NormalizedRelayUrl?,
) = add(UserTag.assemble(pubkey, relayHint))
fun TagArrayBuilder<FollowListEvent>.personFirst(
pubkey: HexKey,
relayHint: NormalizedRelayUrl?,
) = addFirst(UserTag.assemble(pubkey, relayHint))
fun TagArrayBuilder<FollowListEvent>.removePerson(pubkey: HexKey) = remove(UserTag.TAG_NAME, pubkey)
@@ -40,8 +40,8 @@ import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.MuteTag
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.nip51Lists.replaceAll
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
@@ -70,6 +70,8 @@ class PeopleListEvent(
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
fun users() = tags.users()
fun countMutes() = tags.count(MuteTag::isTagged)
@@ -87,7 +89,17 @@ class PeopleListEvent(
fun createBlockAddress(pubKey: HexKey) = Address(KIND, pubKey, BLOCK_LIST_D_TAG)
fun blockListFor(pubKeyHex: HexKey): String = "30000:$pubKeyHex:$BLOCK_LIST_D_TAG"
fun blockListFor(pubKeyHex: HexKey): String = Address.assemble(KIND, pubKeyHex, BLOCK_LIST_D_TAG)
fun createAddress(
pubKey: HexKey,
dTag: String,
) = Address(KIND, pubKey, dTag)
fun listFor(
pubKey: HexKey,
dTag: String,
): String = Address.assemble(KIND, pubKey, dTag)
@OptIn(ExperimentalUuidApi::class)
suspend fun create(
@@ -378,69 +390,5 @@ class PeopleListEvent(
signer = signer,
createdAt = createdAt,
)
suspend fun modifyListName(
earlierVersion: PeopleListEvent,
newName: String,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): PeopleListEvent {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val currentTitle = earlierVersion.tags.first { it[0] == NameTag.TAG_NAME || it[0] == TitleTag.TAG_NAME }
val newTitleTag =
if (currentTitle[0] == NameTag.TAG_NAME) {
NameTag.assemble(newName)
} else {
TitleTag.assemble(newName)
}
return resign(
publicTags = earlierVersion.tags.replaceAll(currentTitle, newTitleTag),
privateTags = privateTags.replaceAll(currentTitle, newTitleTag),
signer = signer,
createdAt = createdAt,
)
}
suspend fun modifyDescription(
earlierVersion: PeopleListEvent,
newDescription: String?,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): PeopleListEvent? {
val privateTags = earlierVersion.privateTags(signer) ?: throw SignerExceptions.UnauthorizedDecryptionException()
val currentDescriptionTag = earlierVersion.tags.firstOrNull { it[0] == DescriptionTag.TAG_NAME }
val currentDescription = currentDescriptionTag?.get(1)
if (currentDescription.equals(newDescription)) {
// Do nothing
return null
} else {
if (newDescription == null || newDescription.isEmpty()) {
return resign(
publicTags = earlierVersion.tags.remove { it[0] == DescriptionTag.TAG_NAME },
privateTags = privateTags.remove { it[0] == DescriptionTag.TAG_NAME },
signer = signer,
createdAt = createdAt,
)
} else {
val newDescriptionTag = DescriptionTag.assemble(newDescription)
return if (currentDescriptionTag == null) {
resign(
publicTags = earlierVersion.tags.plusElement(newDescriptionTag),
privateTags = privateTags,
signer = signer,
createdAt = createdAt,
)
} else {
resign(
publicTags = earlierVersion.tags.replaceAll(currentDescriptionTag, newDescriptionTag),
privateTags = privateTags.replaceAll(currentDescriptionTag, newDescriptionTag),
signer = signer,
createdAt = createdAt,
)
}
}
}
}
}
}
@@ -22,8 +22,14 @@ package com.vitorpamplona.quartz.nip51Lists.peopleList
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.muteList.tags.UserTag
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
fun TagArrayBuilder<PeopleListEvent>.name(name: String) = addUnique(NameTag.assemble(name))
fun TagArrayBuilder<PeopleListEvent>.description(desc: String) = addUnique(DescriptionTag.assemble(desc))
fun TagArrayBuilder<PeopleListEvent>.image(url: String) = addUnique(ImageTag.assemble(url))
fun TagArrayBuilder<PeopleListEvent>.peoples(peoples: List<UserTag>) = addAll(peoples.map { it.toTagArray() })
@@ -29,6 +29,6 @@ class ImageTag {
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
fun assemble(url: String) = arrayOf(TAG_NAME, url)
}
}
@@ -35,7 +35,7 @@ import com.vitorpamplona.quartz.nip01Core.tags.aTag.firstTaggedAddress
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.firstTaggedEvent
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.firstTaggedUser
import com.vitorpamplona.quartz.nip01Core.tags.people.firstTaggedUserId
import com.vitorpamplona.quartz.nip01Core.tags.references.ReferenceTag
import com.vitorpamplona.quartz.nip10Notes.BaseThreadedEvent
import com.vitorpamplona.quartz.nip18Reposts.quotes.QTag
@@ -116,7 +116,7 @@ class HighlightEvent(
fun inUrl() = tags.firstNotNullOfOrNull(ReferenceTag::parse)
fun author() = firstTaggedUser()
fun author() = firstTaggedUserId()
fun quote() = content
@@ -66,18 +66,15 @@ class BlossomServersEvent(
suspend fun updateRelayList(
earlierVersion: BlossomServersEvent,
relays: List<String>,
servers: List<String>,
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): BlossomServersEvent {
val tags =
earlierVersion.tags
.filter { it[0] != "server" }
.plus(
relays.map {
arrayOf("server", it)
},
).toTypedArray()
.plus(servers.map { arrayOf("server", it) })
.toTypedArray()
return signer.sign(createdAt, KIND, tags, earlierVersion.content)
}
@@ -24,6 +24,9 @@ import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.node.ObjectNode
import com.vitorpamplona.quartz.utils.asIntOrNull
import com.vitorpamplona.quartz.utils.asLongOrNull
import com.vitorpamplona.quartz.utils.asTextOrNull
class FilterDeserializer : StdDeserializer<Filter>(Filter::class.java) {
override fun deserialize(
@@ -43,14 +46,14 @@ class ManualFilterDeserializer {
}
return Filter(
ids = jsonObject.get("ids").map { it.asText() },
authors = jsonObject.get("authors").map { it.asText() },
kinds = jsonObject.get("kinds").map { it.asInt() },
tags = tags.associateWith { jsonObject.get(it).map { it.asText() } },
since = jsonObject.get("since").asLong(),
until = jsonObject.get("until").asLong(),
limit = jsonObject.get("limit").asInt(),
search = jsonObject.get("search").asText(),
ids = jsonObject.get("ids").mapNotNull { it.asTextOrNull() },
authors = jsonObject.get("authors").mapNotNull { it.asTextOrNull() },
kinds = jsonObject.get("kinds").mapNotNull { it.asIntOrNull() },
tags = tags.associateWith { jsonObject.get(it).mapNotNull { it.asTextOrNull() } },
since = jsonObject.get("since").asLongOrNull(),
until = jsonObject.get("until").asLongOrNull(),
limit = jsonObject.get("limit").asIntOrNull(),
search = jsonObject.get("search").asTextOrNull(),
)
}
}
@@ -26,6 +26,7 @@ import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponse
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseAck
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseGetRelays
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePong
import com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponsePublicKey
@@ -43,8 +44,7 @@ class BunkerResponseDeserializer : StdDeserializer<BunkerResponse>(BunkerRespons
val error = jsonObject.get("error")?.asText()
if (error != null) {
return com.vitorpamplona.quartz.nip46RemoteSigner.BunkerResponseError
.parse(id, result, error)
return BunkerResponseError.parse(id, result, error)
}
if (result != null) {
@@ -26,6 +26,7 @@ import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceMethod
import com.vitorpamplona.quartz.nip47WalletConnect.Request
import com.vitorpamplona.quartz.utils.asTextOrNull
class RequestDeserializer : StdDeserializer<Request>(Request::class.java) {
override fun deserialize(
@@ -33,7 +34,7 @@ class RequestDeserializer : StdDeserializer<Request>(Request::class.java) {
ctxt: DeserializationContext,
): Request? {
val jsonObject: JsonNode = jp.codec.readTree(jp)
val method = jsonObject.get("method")?.asText()
val method = jsonObject.get("method")?.asTextOrNull()
if (method == "pay_invoice") {
return jp.codec.treeToValue(jsonObject, PayInvoiceMethod::class.java)
@@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.nip47WalletConnect.PayInvoiceSuccessResponse
import com.vitorpamplona.quartz.nip47WalletConnect.Response
import com.vitorpamplona.quartz.utils.asTextOrNull
class ResponseDeserializer : StdDeserializer<Response>(Response::class.java) {
override fun deserialize(
@@ -34,7 +35,7 @@ class ResponseDeserializer : StdDeserializer<Response>(Response::class.java) {
ctxt: DeserializationContext,
): Response? {
val jsonObject: JsonNode = jp.codec.readTree(jp)
val resultType = jsonObject.get("result_type")?.asText()
val resultType = jsonObject.get("result_type")?.asTextOrNull()
if (resultType == "pay_invoice") {
val result = jsonObject.get("result")
@@ -26,6 +26,9 @@ import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.vitorpamplona.quartz.nip01Core.jackson.TagArrayManualDeserializer
import com.vitorpamplona.quartz.nip59Giftwrap.rumors.Rumor
import com.vitorpamplona.quartz.utils.asIntOrNull
import com.vitorpamplona.quartz.utils.asLongOrNull
import com.vitorpamplona.quartz.utils.asTextOrNull
class RumorDeserializer : StdDeserializer<Rumor>(Rumor::class.java) {
override fun deserialize(
@@ -34,12 +37,12 @@ class RumorDeserializer : StdDeserializer<Rumor>(Rumor::class.java) {
): Rumor {
val jsonObject: JsonNode = jp.codec.readTree(jp)
return Rumor(
id = jsonObject.get("id")?.asText()?.intern(),
pubKey = jsonObject.get("pubkey")?.asText()?.intern(),
createdAt = jsonObject.get("created_at")?.asLong(),
kind = jsonObject.get("kind")?.asInt(),
id = jsonObject.get("id")?.asTextOrNull()?.intern(),
pubKey = jsonObject.get("pubkey")?.asTextOrNull()?.intern(),
createdAt = jsonObject.get("created_at")?.asLongOrNull(),
kind = jsonObject.get("kind")?.asIntOrNull(),
tags = TagArrayManualDeserializer.fromJson(jsonObject.get("tags")),
content = jsonObject.get("content")?.asText(),
content = jsonObject.get("content")?.asTextOrNull(),
)
}
}
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2025 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.utils
import com.fasterxml.jackson.databind.JsonNode
fun JsonNode.asBooleanOrNull() = if (!this.isNull && this.isBoolean) this.booleanValue() else null
fun JsonNode.asTextOrNull() = if (!this.isNull && this.isTextual) this.textValue() else null
fun JsonNode.asLongOrNull() = if (!this.isNull && this.isNumber) this.longValue() else null
fun JsonNode.asIntOrNull() = if (!this.isNull && this.isNumber) this.intValue() else null
@@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip01Core.relay
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import com.vitorpamplona.quartz.nip01Core.relay.client.NostrClient
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.reqResultsInOrderAsFlow
import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.reqAsFlow
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CoroutineScope
@@ -50,7 +50,7 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() {
val client = NostrClient(socketBuilder, appScope)
val flow =
client.reqResultsInOrderAsFlow(
client.reqAsFlow(
relay = "wss://relay.damus.io",
filter =
Filter(
@@ -88,7 +88,7 @@ class NostrClientSubscriptionAsFlowTest : BaseNostrClientTest() {
val client = NostrClient(socketBuilder, appScope)
val flow =
client.reqResultsInOrderAsFlow(
client.reqAsFlow(
relay = "wss://relay.damus.io",
filter =
Filter(