diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
index 778f85b9a..a6931869c 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/Account.kt
@@ -1349,27 +1349,48 @@ class Account(
) {
if (!isWriteable()) return
- MetadataEvent.updateFromPast(
- latest = userProfile().latestMetadata,
- name = name,
- picture = picture,
- banner = banner,
- website = website,
- pronouns = pronouns,
- about = about,
- nip05 = nip05,
- lnAddress = lnAddress,
- lnURL = lnURL,
- twitter = twitter,
- mastodon = mastodon,
- github = github,
- signer = signer,
- ) {
+ val latest = userProfile().latestMetadata
+
+ val template =
+ if (latest != null) {
+ MetadataEvent.updateFromPast(
+ latest = latest,
+ name = name,
+ displayName = name,
+ picture = picture,
+ banner = banner,
+ website = website,
+ pronouns = pronouns,
+ about = about,
+ nip05 = nip05,
+ lnAddress = lnAddress,
+ lnURL = lnURL,
+ twitter = twitter,
+ mastodon = mastodon,
+ github = github,
+ )
+ } else {
+ MetadataEvent.createNew(
+ name = name,
+ displayName = name,
+ picture = picture,
+ banner = banner,
+ website = website,
+ pronouns = pronouns,
+ about = about,
+ nip05 = nip05,
+ lnAddress = lnAddress,
+ lnURL = lnURL,
+ twitter = twitter,
+ mastodon = mastodon,
+ github = github,
+ )
+ }
+
+ signer.sign(template) {
Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null)
}
-
- return
}
fun reactionTo(
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt
index ff48090e9..26ee67343 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataScreen.kt
@@ -142,7 +142,7 @@ fun NewUserMetadataScreen(
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState()),
) {
OutlinedTextField(
- label = { Text(text = stringRes(R.string.display_name)) },
+ label = { Text(text = stringRes(R.string.profile_name)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.displayName.value,
onValueChange = { postViewModel.displayName.value = it },
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt
index 5763ea36f..803d7f425 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/AccountStateViewModel.kt
@@ -285,7 +285,7 @@ class AccountStateViewModel : ViewModel() {
AccountSettings(
keyPair = keyPair,
transientAccount = false,
- backupUserMetadata = MetadataEvent.newUser(name, tempSigner),
+ backupUserMetadata = tempSigner.sign(MetadataEvent.newUser(name)),
backupContactList =
ContactListEvent.createFromScratch(
followUsers = listOf(ContactTag(keyPair.pubKey.toHexKey(), null, null)),
diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt
index a3d1fbba2..6454778b5 100644
--- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt
+++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt
@@ -68,7 +68,7 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity
-import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaim
+import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag
import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity
import com.vitorpamplona.quartz.nip39ExtIdentities.TelegramIdentity
import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity
@@ -196,7 +196,7 @@ fun DrawAdditionalInfo(
val identities = user.latestMetadata?.identityClaims()
if (!identities.isNullOrEmpty()) {
- identities.forEach { identity: IdentityClaim ->
+ identities.forEach { identity: IdentityClaimTag ->
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
tint = Color.Unspecified,
@@ -241,7 +241,7 @@ fun DrawAdditionalInfo(
DisplayAppRecommendations(appRecommendations, accountViewModel, nav)
}
-fun getIdentityClaimIcon(identity: IdentityClaim): Int =
+fun getIdentityClaimIcon(identity: IdentityClaimTag): Int =
when (identity) {
is TwitterIdentity -> R.drawable.x
is TelegramIdentity -> R.drawable.telegram
@@ -250,7 +250,7 @@ fun getIdentityClaimIcon(identity: IdentityClaim): Int =
else -> R.drawable.github
}
-fun getIdentityClaimDescription(identity: IdentityClaim): Int =
+fun getIdentityClaimDescription(identity: IdentityClaimTag): Int =
when (identity) {
is TwitterIdentity -> R.string.twitter
is TelegramIdentity -> R.string.telegram
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index 45d713956..863e22ea3 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -111,6 +111,7 @@
Global Feed
Search Feed
Add a Relay
+ Name
Display Name
My display name
Ostrich McAwesome
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 957cacd79..322b3ba5a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -121,6 +121,7 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
rfc3986-normalizer = { group = "org.czeal", name = "rfc3986", version.ref = "rfc3986" }
secp256k1-kmp-jni-android = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-android", version.ref = "secp256k1KmpJniAndroid" }
+secp256k1-kmp-jni-jvm = { group = "fr.acinq.secp256k1", name = "secp256k1-kmp-jni-jvm", version.ref = "secp256k1KmpJniAndroid" }
tor-android = { module = "info.guardianproject:tor-android", version.ref = "torAndroid" }
unifiedpush = { group = "com.github.UnifiedPush", name = "android-connector", version.ref = "unifiedpush" }
url-detector = { group = "io.github.url-detector", name = "url-detector", version.ref = "urlDetector" }
diff --git a/quartz/build.gradle b/quartz/build.gradle
index fb70e1d17..ef440c1a9 100644
--- a/quartz/build.gradle
+++ b/quartz/build.gradle
@@ -77,6 +77,7 @@ dependencies {
api libs.rfc3986.normalizer
testImplementation libs.junit
+ testImplementation libs.secp256k1.kmp.jni.jvm
androidTestImplementation platform(libs.androidx.compose.bom)
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt
index 5055ef9df..7c536a4e6 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/core/Event.kt
@@ -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> = emptyArray(),
content: String = "",
createdAt: Long = TimeUtils.now(),
- onReady: (Event) -> Unit,
- ) = signer.sign(createdAt, kind, tags, content, onReady)
+ initializer: TagArrayBuilder.() -> Unit = {},
+ ) = eventTemplate(kind, content, createdAt, initializer)
}
}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/DeterministicSigner.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/DeterministicSigner.kt
new file mode 100644
index 000000000..495d26c41
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/DeterministicSigner.kt
@@ -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 sign(
+ createdAt: Long,
+ kind: Int,
+ tags: Array>,
+ content: String,
+ ): T = EventAssembler.hashAndSign(pubKey, createdAt, kind, tags, content, key.privKey!!, nonce = null)
+
+ fun sign(ev: EventTemplate): T = sign(ev.createdAt, ev.kind, ev.tags, ev.content)
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventAssembler.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventAssembler.kt
new file mode 100644
index 000000000..4ed4133d8
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/crypto/EventAssembler.kt
@@ -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 hashAndSign(
+ pubKey: HexKey,
+ createdAt: Long,
+ kind: Int,
+ tags: Array>,
+ 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
+ }
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt
index 15724e357..2d1940277 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventManualSerializer.kt
@@ -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>,
+ 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>,
+ content: String,
+ sig: String,
+ ): String {
+ val obj = assemble(id, pubKey, createdAt, kind, tags, content, sig)
+ return EventMapper.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj)
+ }
}
}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt
index f3a41e3fd..4cceec24d 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/EventMapper.kt
@@ -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())
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/InliningTagArrayPrettyPrinter.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/InliningTagArrayPrettyPrinter.kt
new file mode 100644
index 000000000..97c34ebfc
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/jackson/InliningTagArrayPrettyPrinter.kt
@@ -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(']')
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt
index fce6362dc..be7172f11 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt
@@ -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>()
-
- tags.add(
- AltTag.assemble("User profile for ${name ?: currentJson.get("name").asText() ?: ""}"),
- )
-
- return signer.sign(createdAt, KIND, tags.toTypedArray(), writer.buffer.toString())
+ initializer: TagArrayBuilder.() -> Unit = {},
+ ): EventTemplate {
+ 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.() -> Unit = {},
+ ): EventTemplate {
// 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.() -> Unit = {},
+ ): EventTemplate {
+ // 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>()
- 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(
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/TagArrayBuilderExt.kt
new file mode 100644
index 000000000..0913027f1
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/TagArrayBuilderExt.kt
@@ -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.name(name: String) = addUnique(NameTag.assemble(name))
+
+fun TagArrayBuilder.displayName(name: String) = addUnique(DisplayNameTag.assemble(name))
+
+fun TagArrayBuilder.picture(picture: String) = addUnique(PictureTag.assemble(picture))
+
+fun TagArrayBuilder.about(about: String) = addUnique(AboutTag.assemble(about))
+
+fun TagArrayBuilder.website(url: String) = addUnique(WebsiteTag.assemble(url))
+
+fun TagArrayBuilder.nip05(nip05: String) = addUnique(Nip05Tag.assemble(nip05))
+
+fun TagArrayBuilder.lud16(lud16: String) = addUnique(Lud16Tag.assemble(lud16))
+
+fun TagArrayBuilder.lud06(lud06: String) = addUnique(Lud06Tag.assemble(lud06))
+
+fun TagArrayBuilder.banner(banner: String) = addUnique(BannerTag.assemble(banner))
+
+fun TagArrayBuilder.pronouns(pronouns: String) = addUnique(PronounsTag.assemble(pronouns))
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/AboutTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/AboutTag.kt
new file mode 100644
index 000000000..dfe15e9b6
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/AboutTag.kt
@@ -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? {
+ 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)
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/BannerTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/BannerTag.kt
new file mode 100644
index 000000000..6189b81c6
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/BannerTag.kt
@@ -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? {
+ 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)
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/DisplayNameTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/DisplayNameTag.kt
new file mode 100644
index 000000000..d96b32d52
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/DisplayNameTag.kt
@@ -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? {
+ 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)
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/Lud06Tag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/Lud06Tag.kt
new file mode 100644
index 000000000..208af5865
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/Lud06Tag.kt
@@ -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? {
+ 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)
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/Lud16Tag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/Lud16Tag.kt
new file mode 100644
index 000000000..120c28f63
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/Lud16Tag.kt
@@ -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? {
+ 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)
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/NameTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/NameTag.kt
new file mode 100644
index 000000000..3b4108d1d
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/NameTag.kt
@@ -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? {
+ 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)
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/Nip05Tag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/Nip05Tag.kt
new file mode 100644
index 000000000..67c33fc8a
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/Nip05Tag.kt
@@ -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? {
+ 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)
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/PictureTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/PictureTag.kt
new file mode 100644
index 000000000..782e27bd8
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/PictureTag.kt
@@ -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? {
+ 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)
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/PronounsTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/PronounsTag.kt
new file mode 100644
index 000000000..5ebf5a382
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/PronounsTag.kt
@@ -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? {
+ 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)
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/WebsiteTag.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/WebsiteTag.kt
new file mode 100644
index 000000000..71468bd63
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/metadata/tags/WebsiteTag.kt
@@ -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? {
+ 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)
+ }
+}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt
index ebf0029ce..112e0eae4 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip01Core/signers/NostrSignerSync.kt
@@ -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 sign(ev: EventTemplate) = signNormal(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(pubKey, createdAt, kind, tags, content, keyPair.privKey)
}
fun nip04Encrypt(
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/EventExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/EventExt.kt
index 0bda989a4..5787f1c73 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/EventExt.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/EventExt.kt
@@ -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 {
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
}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/GitHubIdentity.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/GitHubIdentity.kt
index 522dc88f4..88bbad7dd 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/GitHubIdentity.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/GitHubIdentity.kt
@@ -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
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/IdentityClaim.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/IdentityClaimTag.kt
similarity index 66%
rename from quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/IdentityClaim.kt
rename to quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/IdentityClaimTag.kt
index cfe30c135..449100c19 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/IdentityClaim.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/IdentityClaimTag.kt
@@ -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): 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 = arrayOf(TAG_NAME, platformIdentity, proof)
}
}
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/MastodonIdentity.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/MastodonIdentity.kt
index 59975daf8..57292e8a3 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/MastodonIdentity.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/MastodonIdentity.kt
@@ -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
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TagArrayBuilderExt.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TagArrayBuilderExt.kt
new file mode 100644
index 000000000..4c27d610b
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TagArrayBuilderExt.kt
@@ -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.claims(identities: List) = addAll(identities.map { it.toTagArray() })
+
+fun TagArrayBuilder.twitterClaim(twitter: TwitterIdentity) = add(twitter.toTagArray())
+
+fun TagArrayBuilder.mastodonClaim(mastodon: MastodonIdentity) = add(mastodon.toTagArray())
+
+fun TagArrayBuilder.githubClaim(github: GitHubIdentity) = add(github.toTagArray())
+
+fun TagArrayBuilder.twitterClaim(twitterUrl: String) = TwitterIdentity.parseProofUrl(twitterUrl)?.let { twitterClaim(it) }
+
+fun TagArrayBuilder.mastodonClaim(mastodonUrl: String) = MastodonIdentity.parseProofUrl(mastodonUrl)?.let { mastodonClaim(it) }
+
+fun TagArrayBuilder.githubClaim(githubUrl: String) = GitHubIdentity.parseProofUrl(githubUrl)?.let { githubClaim(it) }
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TelegramIdentity.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TelegramIdentity.kt
index 0dff28a8f..555196e75 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TelegramIdentity.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TelegramIdentity.kt
@@ -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
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TwitterIdentity.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TwitterIdentity.kt
index 0f87cf517..befdb67b5 100644
--- a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TwitterIdentity.kt
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/TwitterIdentity.kt
@@ -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
diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/UnsupportedIdentity.kt b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/UnsupportedIdentity.kt
new file mode 100644
index 000000000..ba55629db
--- /dev/null
+++ b/quartz/src/main/java/com/vitorpamplona/quartz/nip39ExtIdentities/UnsupportedIdentity.kt
@@ -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
+}
diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/jackson/InliningTagArrayPrettyPrinterTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/jackson/InliningTagArrayPrettyPrinterTest.kt
new file mode 100644
index 000000000..f8d071fed
--- /dev/null
+++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/jackson/InliningTagArrayPrettyPrinterTest.kt
@@ -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)
+ }
+}
diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt
new file mode 100644
index 000000000..563dcd081
--- /dev/null
+++ b/quartz/src/test/java/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt
@@ -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())
+ }
+}
diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/utils/SignerUtils.kt b/quartz/src/test/java/com/vitorpamplona/quartz/utils/SignerUtils.kt
new file mode 100644
index 000000000..dafde3c62
--- /dev/null
+++ b/quartz/src/test/java/com/vitorpamplona/quartz/utils/SignerUtils.kt
@@ -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) }