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) }