- Improves signer utils for testing
- Improves test cases for User metadata
This commit is contained in:
Vitor Pamplona
2025-02-27 18:34:10 -05:00
parent 8f0879b8e5
commit 0df07ddfcb
37 changed files with 1277 additions and 161 deletions
@@ -24,7 +24,7 @@ import androidx.compose.runtime.Immutable
import com.fasterxml.jackson.annotation.JsonProperty
import com.vitorpamplona.quartz.nip01Core.jackson.EventManualSerializer
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.pointerSizeInBytes
@@ -52,16 +52,19 @@ open class Event(
fun toJson(): String = EventManualSerializer.toJson(id, pubKey, createdAt, kind, tags, content, sig)
/**
* For debug purposes only
*/
fun toPrettyJson(): String = EventManualSerializer.toPrettyJson(id, pubKey, createdAt, kind, tags, content, sig)
companion object {
fun fromJson(json: String): Event = EventMapper.fromJson(json)
fun create(
signer: NostrSigner,
fun build(
kind: Int,
tags: Array<Array<String>> = emptyArray(),
content: String = "",
createdAt: Long = TimeUtils.now(),
onReady: (Event) -> Unit,
) = signer.sign(createdAt, kind, tags, content, onReady)
initializer: TagArrayBuilder<Event>.() -> Unit = {},
) = eventTemplate(kind, content, createdAt, initializer)
}
}
@@ -0,0 +1,40 @@
/**
* 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.nip01Core.crypto
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
class DeterministicSigner(
val key: KeyPair,
val pubKey: HexKey = key.pubKey.toHexKey(),
) {
fun <T : Event> sign(
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
): T = EventAssembler.hashAndSign(pubKey, createdAt, kind, tags, content, key.privKey!!, nonce = null)
fun <T : Event> sign(ev: EventTemplate<T>): T = sign(ev.createdAt, ev.kind, ev.tags, ev.content)
}
@@ -0,0 +1,54 @@
/**
* 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.nip01Core.crypto
import com.vitorpamplona.quartz.EventFactory
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.utils.RandomInstance
class EventAssembler {
companion object {
fun <T : Event> hashAndSign(
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
privKey: ByteArray,
nonce: ByteArray? = RandomInstance.bytes(32),
): T {
val id = EventHasher.hashIdBytes(pubKey, createdAt, kind, tags, content)
val sig = Nip01.sign(id, privKey, nonce).toHexKey()
return EventFactory.create(
id.toHexKey(),
pubKey,
createdAt,
kind,
tags,
content,
sig,
) as T
}
}
}
@@ -21,10 +21,42 @@
package com.vitorpamplona.quartz.nip01Core.jackson
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.fasterxml.jackson.databind.node.ObjectNode
import com.vitorpamplona.quartz.nip01Core.core.HexKey
class EventManualSerializer {
companion object {
private fun assemble(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
sig: String,
): ObjectNode {
val factory = JsonNodeFactory.instance
return factory.objectNode().apply {
put("id", id)
put("pubkey", pubKey)
put("created_at", createdAt)
put("kind", kind)
replace(
"tags",
factory.arrayNode(tags.size).apply {
tags.forEach { tag ->
add(
factory.arrayNode(tag.size).apply { tag.forEach { add(it) } },
)
}
},
)
put("content", content)
put("sig", sig)
}
}
fun toJson(
id: HexKey,
pubKey: HexKey,
@@ -34,29 +66,24 @@ class EventManualSerializer {
content: String,
sig: String,
): String {
val factory = JsonNodeFactory.instance
val obj =
factory.objectNode().apply {
put("id", id)
put("pubkey", pubKey)
put("created_at", createdAt)
put("kind", kind)
replace(
"tags",
factory.arrayNode(tags.size).apply {
tags.forEach { tag ->
add(
factory.arrayNode(tag.size).apply { tag.forEach { add(it) } },
)
}
},
)
put("content", content)
put("sig", sig)
}
val obj = assemble(id, pubKey, createdAt, kind, tags, content, sig)
return EventMapper.mapper.writeValueAsString(obj)
}
/**
* For debug purposes only
*/
fun toPrettyJson(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: Array<Array<String>>,
content: String,
sig: String,
): String {
val obj = assemble(id, pubKey, createdAt, kind, tags, content, sig)
return EventMapper.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj)
}
}
}
@@ -41,10 +41,13 @@ import com.vitorpamplona.quartz.nip59Giftwrap.rumors.RumorSerializer
class EventMapper {
companion object {
val defaultPrettyPrinter = InliningTagArrayPrettyPrinter()
val mapper =
jacksonObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
.setDefaultPrettyPrinter(defaultPrettyPrinter)
.registerModule(
SimpleModule()
.addSerializer(Event::class.java, EventSerializer())
@@ -0,0 +1,81 @@
/**
* 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.nip01Core.jackson
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.util.DefaultIndenter
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
import com.fasterxml.jackson.core.util.Separators
class InliningTagArrayPrettyPrinter : DefaultPrettyPrinter {
companion object {
val MY_SEPARATORS =
DEFAULT_SEPARATORS
.withObjectFieldValueSpacing(Separators.Spacing.AFTER)
}
init {
indentArraysWith(DefaultIndenter(" ", "\n"))
}
constructor(separators: Separators? = MY_SEPARATORS) : super(separators)
constructor(base: InliningTagArrayPrettyPrinter) : super(base)
override fun createInstance(): DefaultPrettyPrinter = InliningTagArrayPrettyPrinter(this)
override fun writeStartArray(g: JsonGenerator) {
if (!_arrayIndenter.isInline) {
++_nesting
}
g.writeRaw('[')
}
override fun beforeArrayValues(g: JsonGenerator) {
if (_nesting < 3) {
_arrayIndenter.writeIndentation(g, _nesting)
}
}
override fun writeArrayValueSeparator(g: JsonGenerator) {
g.writeRaw(_arrayValueSeparator)
if (_nesting < 3) {
_arrayIndenter.writeIndentation(g, _nesting)
} else {
g.writeRaw(' ')
}
}
override fun writeEndArray(
g: JsonGenerator,
nrOfValues: Int,
) {
if (!_arrayIndenter.isInline) {
--_nesting
}
if (nrOfValues > 0) {
if (_nesting < 2) {
_arrayIndenter.writeIndentation(g, _nesting)
}
}
g.writeRaw(']')
}
}
@@ -21,19 +21,35 @@
package com.vitorpamplona.quartz.nip01Core.metadata
import android.util.Log
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.JsonNodeFactory
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.builder
import com.vitorpamplona.quartz.nip01Core.jackson.EventMapper
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
import com.vitorpamplona.quartz.nip01Core.metadata.tags.AboutTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.BannerTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.DisplayNameTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud06Tag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud16Tag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.NameTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.Nip05Tag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.PictureTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.PronounsTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.WebsiteTag
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip21UriScheme.toNostrUri
import com.vitorpamplona.quartz.nip31Alts.AltTag
import com.vitorpamplona.quartz.nip39ExtIdentities.updateClaims
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag
import com.vitorpamplona.quartz.nip39ExtIdentities.claims
import com.vitorpamplona.quartz.nip39ExtIdentities.githubClaim
import com.vitorpamplona.quartz.nip39ExtIdentities.mastodonClaim
import com.vitorpamplona.quartz.nip39ExtIdentities.replaceClaims
import com.vitorpamplona.quartz.nip39ExtIdentities.twitterClaim
import com.vitorpamplona.quartz.utils.TimeUtils
import java.io.ByteArrayInputStream
import java.io.StringWriter
class MetadataEvent(
id: HexKey,
@@ -43,11 +59,12 @@ class MetadataEvent(
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
fun contactMetadataJson() = jacksonObjectMapper().readTree(content) as? ObjectNode
fun contactMetaData() =
try {
EventMapper.mapper.readValue(content, UserMetadata::class.java)
} catch (e: Exception) {
// e.printStackTrace()
Log.w("MetadataEvent", "Content Parse Error: ${toNostrUri()} ${e.localizedMessage}")
null
}
@@ -57,76 +74,130 @@ class MetadataEvent(
fun newUser(
name: String?,
signer: NostrSignerSync,
createdAt: Long = TimeUtils.now(),
): MetadataEvent? {
// Tries to not delete any existing attribute that we do not work with.
val currentJson = ObjectMapper().createObjectNode()
name?.let { addIfNotBlank(currentJson, "name", it.trim()) }
val writer = StringWriter()
ObjectMapper().writeValue(writer, currentJson)
val tags = mutableListOf<Array<String>>()
tags.add(
AltTag.assemble("User profile for ${name ?: currentJson.get("name").asText() ?: ""}"),
)
return signer.sign(createdAt, KIND, tags.toTypedArray(), writer.buffer.toString())
initializer: TagArrayBuilder<MetadataEvent>.() -> Unit = {},
): EventTemplate<MetadataEvent> {
val metadata = JsonNodeFactory.instance.objectNode()
name?.let { addIfNotBlank(metadata, "name", it.trim()) }
return eventTemplate(KIND, metadata.toString(), createdAt) {
alt("User profile for $name")
initializer()
}
}
fun updateFromPast(
latest: MetadataEvent?,
name: String?,
picture: String?,
banner: String?,
website: String?,
about: String?,
nip05: String?,
lnAddress: String?,
lnURL: String?,
pronouns: String?,
twitter: String?,
mastodon: String?,
github: String?,
signer: NostrSigner,
fun createNew(
name: String? = null,
displayName: String? = null,
picture: String? = null,
banner: String? = null,
website: String? = null,
about: String? = null,
nip05: String? = null,
lnAddress: String? = null,
lnURL: String? = null,
pronouns: String? = null,
twitter: String? = null,
mastodon: String? = null,
github: String? = null,
createdAt: Long = TimeUtils.now(),
onReady: (MetadataEvent) -> Unit,
) {
initializer: TagArrayBuilder<MetadataEvent>.() -> Unit = {},
): EventTemplate<MetadataEvent> {
// Tries to not delete any existing attribute that we do not work with.
val currentJson =
if (latest != null) {
ObjectMapper()
.readTree(
ByteArrayInputStream(latest.content.toByteArray(Charsets.UTF_8)),
) as ObjectNode
} else {
ObjectMapper().createObjectNode()
val currentMetadata = JsonNodeFactory.instance.objectNode()
name?.let { addIfNotBlank(currentMetadata, NameTag.TAG_NAME, it.trim()) }
displayName?.let { addIfNotBlank(currentMetadata, DisplayNameTag.TAG_NAME, it.trim()) }
picture?.let { addIfNotBlank(currentMetadata, PictureTag.TAG_NAME, it.trim()) }
banner?.let { addIfNotBlank(currentMetadata, BannerTag.TAG_NAME, it.trim()) }
website?.let { addIfNotBlank(currentMetadata, WebsiteTag.TAG_NAME, it.trim()) }
pronouns?.let { addIfNotBlank(currentMetadata, PronounsTag.TAG_NAME, it.trim()) }
about?.let { addIfNotBlank(currentMetadata, AboutTag.TAG_NAME, it.trim()) }
nip05?.let { addIfNotBlank(currentMetadata, Nip05Tag.TAG_NAME, it.trim()) }
lnAddress?.let { addIfNotBlank(currentMetadata, Lud16Tag.TAG_NAME, it.trim()) }
lnURL?.let { addIfNotBlank(currentMetadata, Lud06Tag.TAG_NAME, it.trim()) }
return eventTemplate(KIND, currentMetadata.toString(), createdAt) {
alt("User profile for ${currentMetadata.get("name").asText() ?: "Anonymous"}")
// For https://github.com/nostr-protocol/nips/pull/1770
currentMetadata.get(NameTag.TAG_NAME)?.asText()?.let { name(it) }
currentMetadata.get(DisplayNameTag.TAG_NAME)?.asText()?.let { displayName(it) }
currentMetadata.get(PictureTag.TAG_NAME)?.asText()?.let { picture(it) }
currentMetadata.get(BannerTag.TAG_NAME)?.asText()?.let { banner(it) }
currentMetadata.get(WebsiteTag.TAG_NAME)?.asText()?.let { website(it) }
currentMetadata.get(PronounsTag.TAG_NAME)?.asText()?.let { pronouns(it) }
currentMetadata.get(AboutTag.TAG_NAME)?.asText()?.let { about(it) }
currentMetadata.get(Nip05Tag.TAG_NAME)?.asText()?.let { nip05(it) }
currentMetadata.get(Lud16Tag.TAG_NAME)?.asText()?.let { lud16(it) }
currentMetadata.get(Lud06Tag.TAG_NAME)?.asText()?.let { lud06(it) }
twitter?.let { twitterClaim(it) }
?: mastodon?.let { mastodonClaim(it) }
github?.let { githubClaim(it) }
initializer()
}
}
/**
* Updates fields from the latest Metadata Event. Null params remain unchanged. Empty params get deleted.
*/
fun updateFromPast(
latest: MetadataEvent,
name: String? = null,
displayName: String? = null,
picture: String? = null,
banner: String? = null,
website: String? = null,
about: String? = null,
nip05: String? = null,
lnAddress: String? = null,
lnURL: String? = null,
pronouns: String? = null,
twitter: String? = null,
mastodon: String? = null,
github: String? = null,
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<MetadataEvent>.() -> Unit = {},
): EventTemplate<MetadataEvent> {
// Tries to not delete any existing attribute that we do not work with.
val currentMetadata = latest.contactMetadataJson() ?: JsonNodeFactory.instance.objectNode()
name?.let { addIfNotBlank(currentMetadata, NameTag.TAG_NAME, it.trim()) }
displayName?.let { addIfNotBlank(currentMetadata, DisplayNameTag.TAG_NAME, it.trim()) }
picture?.let { addIfNotBlank(currentMetadata, PictureTag.TAG_NAME, it.trim()) }
banner?.let { addIfNotBlank(currentMetadata, BannerTag.TAG_NAME, it.trim()) }
website?.let { addIfNotBlank(currentMetadata, WebsiteTag.TAG_NAME, it.trim()) }
pronouns?.let { addIfNotBlank(currentMetadata, PronounsTag.TAG_NAME, it.trim()) }
about?.let { addIfNotBlank(currentMetadata, AboutTag.TAG_NAME, it.trim()) }
nip05?.let { addIfNotBlank(currentMetadata, Nip05Tag.TAG_NAME, it.trim()) }
lnAddress?.let { addIfNotBlank(currentMetadata, Lud16Tag.TAG_NAME, it.trim()) }
lnURL?.let { addIfNotBlank(currentMetadata, Lud06Tag.TAG_NAME, it.trim()) }
val tags =
latest.tags.builder {
alt("User profile for ${currentMetadata.get("name").asText() ?: "Anonymous"}")
// For https://github.com/nostr-protocol/nips/pull/1770
currentMetadata.get(NameTag.TAG_NAME)?.asText()?.let { name(it) } ?: run { remove(NameTag.TAG_NAME) }
currentMetadata.get(DisplayNameTag.TAG_NAME)?.asText()?.let { displayName(it) } ?: run { remove(DisplayNameTag.TAG_NAME) }
currentMetadata.get(PictureTag.TAG_NAME)?.asText()?.let { picture(it) } ?: run { remove(PictureTag.TAG_NAME) }
currentMetadata.get(BannerTag.TAG_NAME)?.asText()?.let { banner(it) } ?: run { remove(BannerTag.TAG_NAME) }
currentMetadata.get(WebsiteTag.TAG_NAME)?.asText()?.let { website(it) } ?: run { remove(WebsiteTag.TAG_NAME) }
currentMetadata.get(PronounsTag.TAG_NAME)?.asText()?.let { pronouns(it) } ?: run { remove(PronounsTag.TAG_NAME) }
currentMetadata.get(AboutTag.TAG_NAME)?.asText()?.let { about(it) } ?: run { remove(AboutTag.TAG_NAME) }
currentMetadata.get(Nip05Tag.TAG_NAME)?.asText()?.let { nip05(it) } ?: run { remove(Nip05Tag.TAG_NAME) }
currentMetadata.get(Lud16Tag.TAG_NAME)?.asText()?.let { lud16(it) } ?: run { remove(Lud16Tag.TAG_NAME) }
currentMetadata.get(Lud06Tag.TAG_NAME)?.asText()?.let { lud06(it) } ?: run { remove(Lud06Tag.TAG_NAME) }
val newClaims = latest.replaceClaims(twitter, mastodon, github)
remove(IdentityClaimTag.TAG_NAME)
claims(newClaims)
initializer()
}
name?.let { addIfNotBlank(currentJson, "name", it.trim()) }
name?.let { addIfNotBlank(currentJson, "display_name", it.trim()) }
picture?.let { addIfNotBlank(currentJson, "picture", it.trim()) }
banner?.let { addIfNotBlank(currentJson, "banner", it.trim()) }
website?.let { addIfNotBlank(currentJson, "website", it.trim()) }
pronouns?.let { addIfNotBlank(currentJson, "pronouns", it.trim()) }
about?.let { addIfNotBlank(currentJson, "about", it.trim()) }
nip05?.let { addIfNotBlank(currentJson, "nip05", it.trim()) }
lnAddress?.let { addIfNotBlank(currentJson, "lud16", it.trim()) }
lnURL?.let { addIfNotBlank(currentJson, "lud06", it.trim()) }
val writer = StringWriter()
ObjectMapper().writeValue(writer, currentJson)
val tags = mutableListOf<Array<String>>()
tags.add(AltTag.assemble("User profile for ${name ?: currentJson.get("name").asText() ?: ""}"))
latest?.updateClaims(twitter, github, mastodon)?.forEach {
tags.add(it)
}
signer.sign(createdAt, KIND, tags.toTypedArray(), writer.buffer.toString(), onReady)
return EventTemplate(createdAt, KIND, tags, currentMetadata.toString())
}
private fun addIfNotBlank(
@@ -0,0 +1,53 @@
/**
* 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.nip01Core.metadata
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.metadata.tags.AboutTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.BannerTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.DisplayNameTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud06Tag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud16Tag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.NameTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.Nip05Tag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.PictureTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.PronounsTag
import com.vitorpamplona.quartz.nip01Core.metadata.tags.WebsiteTag
fun TagArrayBuilder<MetadataEvent>.name(name: String) = addUnique(NameTag.assemble(name))
fun TagArrayBuilder<MetadataEvent>.displayName(name: String) = addUnique(DisplayNameTag.assemble(name))
fun TagArrayBuilder<MetadataEvent>.picture(picture: String) = addUnique(PictureTag.assemble(picture))
fun TagArrayBuilder<MetadataEvent>.about(about: String) = addUnique(AboutTag.assemble(about))
fun TagArrayBuilder<MetadataEvent>.website(url: String) = addUnique(WebsiteTag.assemble(url))
fun TagArrayBuilder<MetadataEvent>.nip05(nip05: String) = addUnique(Nip05Tag.assemble(nip05))
fun TagArrayBuilder<MetadataEvent>.lud16(lud16: String) = addUnique(Lud16Tag.assemble(lud16))
fun TagArrayBuilder<MetadataEvent>.lud06(lud06: String) = addUnique(Lud06Tag.assemble(lud06))
fun TagArrayBuilder<MetadataEvent>.banner(banner: String) = addUnique(BannerTag.assemble(banner))
fun TagArrayBuilder<MetadataEvent>.pronouns(pronouns: String) = addUnique(PronounsTag.assemble(pronouns))
@@ -0,0 +1,38 @@
/**
* 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.nip01Core.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
class AboutTag {
companion object {
const val TAG_NAME = "about"
fun parse(tag: Array<String>): String? {
ensure(tag.size > 1) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -0,0 +1,38 @@
/**
* 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.nip01Core.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
class BannerTag {
companion object {
const val TAG_NAME = "banner"
fun parse(tag: Array<String>): String? {
ensure(tag.size > 1) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -0,0 +1,38 @@
/**
* 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.nip01Core.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
class DisplayNameTag {
companion object {
const val TAG_NAME = "display_name"
fun parse(tag: Array<String>): String? {
ensure(tag.size > 1) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -0,0 +1,38 @@
/**
* 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.nip01Core.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
class Lud06Tag {
companion object {
const val TAG_NAME = "lud06"
fun parse(tag: Array<String>): String? {
ensure(tag.size > 1) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -0,0 +1,38 @@
/**
* 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.nip01Core.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
class Lud16Tag {
companion object {
const val TAG_NAME = "lud16"
fun parse(tag: Array<String>): String? {
ensure(tag.size > 1) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -0,0 +1,38 @@
/**
* 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.nip01Core.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
class NameTag {
companion object {
const val TAG_NAME = "name"
fun parse(tag: Array<String>): String? {
ensure(tag.size > 1) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -0,0 +1,38 @@
/**
* 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.nip01Core.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
class Nip05Tag {
companion object {
const val TAG_NAME = "nip05"
fun parse(tag: Array<String>): String? {
ensure(tag.size > 1) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -0,0 +1,38 @@
/**
* 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.nip01Core.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
class PictureTag {
companion object {
const val TAG_NAME = "picture"
fun parse(tag: Array<String>): String? {
ensure(tag.size > 1) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -0,0 +1,38 @@
/**
* 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.nip01Core.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
class PronounsTag {
companion object {
const val TAG_NAME = "pronouns"
fun parse(tag: Array<String>): String? {
ensure(tag.size > 1) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -0,0 +1,38 @@
/**
* 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.nip01Core.metadata.tags
import com.vitorpamplona.quartz.utils.ensure
class WebsiteTag {
companion object {
const val TAG_NAME = "website"
fun parse(tag: Array<String>): String? {
ensure(tag.size > 1) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(name: String) = arrayOf(TAG_NAME, name)
}
}
@@ -21,14 +21,12 @@
package com.vitorpamplona.quartz.nip01Core.signers
import android.util.Log
import com.vitorpamplona.quartz.EventFactory
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.EventHasher
import com.vitorpamplona.quartz.nip01Core.crypto.EventAssembler
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.crypto.Nip01
import com.vitorpamplona.quartz.nip04Dm.crypto.Nip04
import com.vitorpamplona.quartz.nip44Encryption.Nip44
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
@@ -36,7 +34,7 @@ import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
import com.vitorpamplona.quartz.nip57Zaps.PrivateZapRequestBuilder
class NostrSignerSync(
val keyPair: KeyPair,
val keyPair: KeyPair = KeyPair(),
val pubKey: HexKey = keyPair.pubKey.toHexKey(),
) {
fun <T : Event> sign(ev: EventTemplate<T>) = signNormal<T>(ev.createdAt, ev.kind, ev.tags, ev.content)
@@ -72,18 +70,7 @@ class NostrSignerSync(
): T? {
if (keyPair.privKey == null) return null
val id = EventHasher.hashIdBytes(pubKey, createdAt, kind, tags, content)
val sig = Nip01.sign(id, keyPair.privKey).toHexKey()
return EventFactory.create(
id.toHexKey(),
pubKey,
createdAt,
kind,
tags,
content,
sig,
) as T
return EventAssembler.hashAndSign<T>(pubKey, createdAt, kind, tags, content, keyPair.privKey)
}
fun nip04Encrypt(
@@ -20,26 +20,15 @@
*/
package com.vitorpamplona.quartz.nip39ExtIdentities
import android.util.Log
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.mapTagged
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
fun MetadataEvent.identityClaims() =
tags.mapTagged("i") {
try {
IdentityClaim.create(it[1], it[2])
} catch (e: Exception) {
Log.e("MetadataEvent", "Can't parse identity [${it.joinToString { "," }}]", e)
null
}
}
fun MetadataEvent.identityClaims() = tags.mapNotNull(IdentityClaimTag::parse)
fun MetadataEvent.updateClaims(
fun MetadataEvent.replaceClaims(
twitter: String?,
mastodon: String?,
github: String?,
): TagArray {
): List<IdentityClaimTag> {
var claims = identityClaims()
// null leave as is. blank deletes it.
@@ -68,5 +57,5 @@ fun MetadataEvent.updateClaims(
}
}
return claims.map { arrayOf("i", it.platformIdentity(), it.proof) }.toTypedArray()
return claims
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip39ExtIdentities
class GitHubIdentity(
identity: String,
proof: String,
) : IdentityClaim(identity, proof) {
) : IdentityClaimTag(identity, proof) {
override fun toProofUrl() = "https://gist.github.com/$identity/$proof"
override fun platform() = platform
@@ -20,10 +20,12 @@
*/
package com.vitorpamplona.quartz.nip39ExtIdentities
import android.util.Log
import androidx.compose.runtime.Stable
import com.vitorpamplona.quartz.utils.ensure
@Stable
abstract class IdentityClaim(
abstract class IdentityClaimTag(
val identity: String,
val proof: String,
) {
@@ -31,13 +33,17 @@ abstract class IdentityClaim(
abstract fun platform(): String
fun toTagArray() = assemble(platformIdentity(), proof)
fun platformIdentity() = "${platform()}:$identity"
companion object {
const val TAG_NAME = "i"
fun create(
platformIdentity: String,
proof: String,
): IdentityClaim {
): IdentityClaimTag {
val (platform, identity) = platformIdentity.split(':')
return when (platform.lowercase()) {
@@ -45,8 +51,26 @@ abstract class IdentityClaim(
TwitterIdentity.platform -> TwitterIdentity(identity, proof)
TelegramIdentity.platform -> TelegramIdentity(identity, proof)
MastodonIdentity.platform -> MastodonIdentity(identity, proof)
else -> throw IllegalArgumentException("Platform $platform not supported")
else -> UnsupportedIdentity(platform, identity, proof)
}
}
fun parse(tag: Array<String>): IdentityClaimTag? {
ensure(tag.size > 2) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
ensure(tag[2].isNotEmpty()) { return null }
return try {
create(tag[1], tag[2])
} catch (e: Exception) {
Log.e("IdentityClaim", "Can't parse identity [${tag.joinToString { "," }}]", e)
null
}
}
fun assemble(
platformIdentity: String,
proof: String,
): Array<String> = arrayOf(TAG_NAME, platformIdentity, proof)
}
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip39ExtIdentities
class MastodonIdentity(
identity: String,
proof: String,
) : IdentityClaim(identity, proof) {
) : IdentityClaimTag(identity, proof) {
override fun toProofUrl() = "https://$identity/$proof"
override fun platform() = platform
@@ -0,0 +1,38 @@
/**
* 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.nip39ExtIdentities
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
fun TagArrayBuilder<MetadataEvent>.claims(identities: List<IdentityClaimTag>) = addAll(identities.map { it.toTagArray() })
fun TagArrayBuilder<MetadataEvent>.twitterClaim(twitter: TwitterIdentity) = add(twitter.toTagArray())
fun TagArrayBuilder<MetadataEvent>.mastodonClaim(mastodon: MastodonIdentity) = add(mastodon.toTagArray())
fun TagArrayBuilder<MetadataEvent>.githubClaim(github: GitHubIdentity) = add(github.toTagArray())
fun TagArrayBuilder<MetadataEvent>.twitterClaim(twitterUrl: String) = TwitterIdentity.parseProofUrl(twitterUrl)?.let { twitterClaim(it) }
fun TagArrayBuilder<MetadataEvent>.mastodonClaim(mastodonUrl: String) = MastodonIdentity.parseProofUrl(mastodonUrl)?.let { mastodonClaim(it) }
fun TagArrayBuilder<MetadataEvent>.githubClaim(githubUrl: String) = GitHubIdentity.parseProofUrl(githubUrl)?.let { githubClaim(it) }
@@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip39ExtIdentities
class TelegramIdentity(
identity: String,
proof: String,
) : IdentityClaim(identity, proof) {
) : IdentityClaimTag(identity, proof) {
override fun toProofUrl() = "https://t.me/$proof"
override fun platform() = platform
@@ -23,7 +23,7 @@ package com.vitorpamplona.quartz.nip39ExtIdentities
class TwitterIdentity(
identity: String,
proof: String,
) : IdentityClaim(identity, proof) {
) : IdentityClaimTag(identity, proof) {
override fun toProofUrl() = "https://x.com/$identity/status/$proof"
override fun platform() = platform
@@ -0,0 +1,31 @@
/**
* 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.nip39ExtIdentities
class UnsupportedIdentity(
val platform: String,
identity: String,
proof: String,
) : IdentityClaimTag(identity, proof) {
override fun toProofUrl() = "Unsupported Identity"
override fun platform() = platform
}
@@ -0,0 +1,95 @@
/**
* 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.nip01Core.jackson
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.junit.Assert.assertEquals
import org.junit.Test
class InliningTagArrayPrettyPrinterTest {
val mapper =
jacksonObjectMapper().apply {
setDefaultPrettyPrinter(InliningTagArrayPrettyPrinter())
}
@Test
fun test() {
val writer = mapper.writerWithDefaultPrettyPrinter()
val data =
mapOf(
"tags" to arrayOf(intArrayOf(1, 2, 3), intArrayOf(4, 5, 6), intArrayOf(7, 8, 9)),
)
val expected =
"""
{
"tags": [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
}
""".trimIndent()
val json = writer.writeValueAsString(data)
assertEquals(expected, json)
val data2 =
mapOf(
"tags" to arrayOf(arrayOf(intArrayOf(1, 2), intArrayOf(3, 4)), arrayOf(intArrayOf(5, 6), intArrayOf(7, 8))),
)
val expected2 =
"""
{
"tags": [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]]
]
}
""".trimIndent()
val json2 = writer.writeValueAsString(data2)
assertEquals(expected2, json2)
}
@Test
fun testEvent() {
val nostrObject =
"""
{
"id": "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe",
"pubkey": "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
"created_at": 1740669816,
"kind": 0,
"tags": [
["alt", "User profile for Vitor"],
["name", "Vitor"]
],
"content": "{\"name\":\"Vitor\"}",
"sig": "977a6152199f17d103d8d56736ed1b7767054464cf9423d017c01c8cdd2344698f0a5e13da8dff98d01bb1f798837e3b6271e1fd1cac861bb90686f622ae6ef4"
}
""".trimIndent()
val tree = mapper.readTree(nostrObject)
val prettified = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(tree)
assertEquals(nostrObject, prettified)
}
}
@@ -0,0 +1,187 @@
/**
* 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.nip01Core.metadata
import com.vitorpamplona.quartz.utils.nsecToSigner
import org.junit.Assert.assertEquals
import org.junit.Test
class UpdateMetadataTest {
val signer = "nsec10g0wheggqn9dawlc0yuv6adnat6n09anr7eyykevw2dm8xa5fffs0wsdsr".nsecToSigner()
@Test
fun createNewMetadata() {
val test = signer.sign(MetadataEvent.createNew("Vitor", createdAt = 1740669816))
val expected =
"""
{
"id": "490d7439e530423f2540d4f2bdb73a0a2935f3df9e1f2a6f699a140c7db311fe",
"pubkey": "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
"created_at": 1740669816,
"kind": 0,
"tags": [
["alt", "User profile for Vitor"],
["name", "Vitor"]
],
"content": "{\"name\":\"Vitor\"}",
"sig": "977a6152199f17d103d8d56736ed1b7767054464cf9423d017c01c8cdd2344698f0a5e13da8dff98d01bb1f798837e3b6271e1fd1cac861bb90686f622ae6ef4"
}
""".trimIndent()
assertEquals(expected, test.toPrettyJson())
}
@Test
fun updateMetadata() {
val test =
signer.sign(
MetadataEvent.createNew(
name = "Vitor",
displayName = "Vitor Pamplona",
about = "Nostr's Chief Android Officer - #Amethyst",
picture = "https://vitorpamplona.com/images/me_300.jpg",
banner = "https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360",
pronouns = "he/him",
website = "https://vitorpamplona.com",
nip05 = "_@vitorpamplona.com",
lnAddress = "vitor@vitorpamplona.com",
lnURL = "TEST",
github = "https://gist.github.com/vitorpamplona/cf19e2d1d7f8dac6348ad37b35ec8421",
createdAt = 1740669816,
),
)
val expected =
"""
{
"id": "2b2761d66db4a83d5fb7a98cafb8414e9b0c238ceb67d729bedacc1a092d516f",
"pubkey": "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
"created_at": 1740669816,
"kind": 0,
"tags": [
["alt", "User profile for Vitor"],
["name", "Vitor"],
["display_name", "Vitor Pamplona"],
["picture", "https://vitorpamplona.com/images/me_300.jpg"],
["banner", "https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360"],
["website", "https://vitorpamplona.com"],
["pronouns", "he/him"],
["about", "Nostr's Chief Android Officer - #Amethyst"],
["nip05", "_@vitorpamplona.com"],
["lud16", "vitor@vitorpamplona.com"],
["lud06", "TEST"],
["i", "github:vitorpamplona", "cf19e2d1d7f8dac6348ad37b35ec8421"]
],
"content": "{\"name\":\"Vitor\",\"display_name\":\"Vitor Pamplona\",\"picture\":\"https://vitorpamplona.com/images/me_300.jpg\",\"banner\":\"https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360\",\"website\":\"https://vitorpamplona.com\",\"pronouns\":\"he/him\",\"about\":\"Nostr's Chief Android Officer - #Amethyst\",\"nip05\":\"_@vitorpamplona.com\",\"lud16\":\"vitor@vitorpamplona.com\",\"lud06\":\"TEST\"}",
"sig": "0a8c78eb0c5e0ba46e4781cc445fb7b6d275b434cead9231bd19b4f95671e3ab872264e50d4456d6036a84cc81e517bfa24229571519aabefcbce431e0c7163e"
}
""".trimIndent()
assertEquals(expected, test.toPrettyJson())
val expected2 =
"""
{
"id": "2e1e57fae4e4baddac025ea0b49afc093f2aa27610a05e584184ed26b29d7590",
"pubkey": "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
"created_at": 1740669817,
"kind": 0,
"tags": [
["alt", "User profile for 2 Vitor"],
["name", "2 Vitor"],
["display_name", "2 Vitor Pamplona"],
["picture", "2 https://vitorpamplona.com/images/me_300.jpg"],
["banner", "2 https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360"],
["website", "2 https://vitorpamplona.com"],
["pronouns", "2 he/him"],
["about", "2 Nostr's Chief Android Officer - #Amethyst"],
["nip05", "2 _@vitorpamplona.com"],
["lud16", "2 vitor@vitorpamplona.com"],
["lud06", "2 TEST"],
["i", "github:vitorpamplona", "2cf19e2d1d7f8dac6348ad37b35ec8421"]
],
"content": "{\"name\":\"2 Vitor\",\"display_name\":\"2 Vitor Pamplona\",\"picture\":\"2 https://vitorpamplona.com/images/me_300.jpg\",\"banner\":\"2 https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360\",\"website\":\"2 https://vitorpamplona.com\",\"pronouns\":\"2 he/him\",\"about\":\"2 Nostr's Chief Android Officer - #Amethyst\",\"nip05\":\"2 _@vitorpamplona.com\",\"lud16\":\"2 vitor@vitorpamplona.com\",\"lud06\":\"2 TEST\"}",
"sig": "a25483bc0fcc79ccd337e3ff846351097109fc13f1cd1c9cc15f7f2ad46417aeb7c05c1524aeadbab93036fc0743c8c310a5b2b7b482e1808649b12a3ee29d49"
}
""".trimIndent()
val test2 =
signer.sign(
MetadataEvent.updateFromPast(
latest = test,
name = "2 Vitor",
displayName = "2 Vitor Pamplona",
about = "2 Nostr's Chief Android Officer - #Amethyst",
picture = "2 https://vitorpamplona.com/images/me_300.jpg",
banner = "2 https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360",
pronouns = "2 he/him",
website = "2 https://vitorpamplona.com",
nip05 = "2 _@vitorpamplona.com",
lnAddress = "2 vitor@vitorpamplona.com",
lnURL = "2 TEST",
github = "https://gist.github.com/vitorpamplona/2cf19e2d1d7f8dac6348ad37b35ec8421",
createdAt = 1740669817,
),
)
assertEquals(expected2, test2.toPrettyJson())
val expected3 =
"""
{
"id": "94879b9a27fecf1337ede32013006ec4dfd5a3286a1e819abddfa1c3c132f008",
"pubkey": "70a9b3c312a6b83e476739bd29d60ca700da1d5b982cbca87b5f3d27d4038d67",
"created_at": 1740669817,
"kind": 0,
"tags": [
["alt", "User profile for 2 Vitor"],
["name", "2 Vitor"],
["picture", "2 https://vitorpamplona.com/images/me_300.jpg"],
["banner", "2 https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360"]
],
"content": "{\"name\":\"2 Vitor\",\"picture\":\"2 https://vitorpamplona.com/images/me_300.jpg\",\"banner\":\"2 https://pbs.twimg.com/profile_banners/15064756/1414451651/1080x360\"}",
"sig": "729ee02364b4d429b6a66400ec850e80424602e3981ff2ad8a14d61bdee04f76ec68ddc4a0f38b0527f0218f7fa67668012a5c0f3c8ef943517b504040caa07e"
}
""".trimIndent()
val test3 =
signer.sign(
MetadataEvent.updateFromPast(
latest = test2,
name = null,
displayName = "",
about = "",
picture = null,
banner = null,
pronouns = "",
website = "",
nip05 = "",
lnAddress = "",
lnURL = "",
github = "",
createdAt = 1740669817,
),
)
assertEquals(expected3, test3.toPrettyJson())
}
}
@@ -0,0 +1,29 @@
/**
* 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.utils
import com.vitorpamplona.quartz.nip01Core.crypto.DeterministicSigner
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip19Bech32.bech32.bechToBytes
fun String.nsecToKeyPair() = KeyPair(this.bechToBytes())
fun String.nsecToSigner() = this.nsecToKeyPair().let { DeterministicSigner(it) }