diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt index 5fa10c421f..08f00d54ad 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/MetadataEvent.kt @@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder import com.vitorpamplona.quartz.nip01Core.core.builder import com.vitorpamplona.quartz.nip01Core.metadata.tags.AboutTag import com.vitorpamplona.quartz.nip01Core.metadata.tags.BannerTag +import com.vitorpamplona.quartz.nip01Core.metadata.tags.ClinkOfferTag import com.vitorpamplona.quartz.nip01Core.metadata.tags.DisplayNameTag import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud06Tag import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud16Tag @@ -88,9 +89,6 @@ class MetadataEvent( const val KIND = 0 const val FIXED_D_TAG = "" - // CLINK Offers discovery key in kind-0 content (mirrors the NIP-05 `clink_offer` key). - const val CLINK_OFFER_PROPERTY = "clink_offer" - fun createAddress(pubKey: HexKey): Address = Address(KIND, pubKey, FIXED_D_TAG) fun createAddressATag(pubKey: HexKey): ATag = ATag(KIND, pubKey, FIXED_D_TAG, null) @@ -252,7 +250,7 @@ class MetadataEvent( nip05?.let { addIfNotBlank(currentMetadata, Nip05Tag.TAG_NAME, it) } lnAddress?.let { addIfNotBlank(currentMetadata, Lud16Tag.TAG_NAME, it) } lnURL?.let { addIfNotBlank(currentMetadata, Lud06Tag.TAG_NAME, it) } - clinkOffer?.let { addIfNotBlank(currentMetadata, CLINK_OFFER_PROPERTY, it) } + clinkOffer?.let { addIfNotBlank(currentMetadata, ClinkOfferTag.TAG_NAME, it) } } // For https://github.com/nostr-protocol/nips/pull/1770 @@ -267,6 +265,7 @@ class MetadataEvent( currentMetadata[Nip05Tag.TAG_NAME]?.let { nip05(it.text) } ?: run { remove(Nip05Tag.TAG_NAME) } currentMetadata[Lud16Tag.TAG_NAME]?.let { lud16(it.text) } ?: run { remove(Lud16Tag.TAG_NAME) } currentMetadata[Lud06Tag.TAG_NAME]?.let { lud06(it.text) } ?: run { remove(Lud06Tag.TAG_NAME) } + currentMetadata[ClinkOfferTag.TAG_NAME]?.let { clinkOffer(it.text) } ?: run { remove(ClinkOfferTag.TAG_NAME) } } private fun addIfNotBlank( diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/TagArrayBuilderExt.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/TagArrayBuilderExt.kt index 51e37b3d84..163e0df3de 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/TagArrayBuilderExt.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/TagArrayBuilderExt.kt @@ -23,6 +23,7 @@ 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.ClinkOfferTag import com.vitorpamplona.quartz.nip01Core.metadata.tags.DisplayNameTag import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud06Tag import com.vitorpamplona.quartz.nip01Core.metadata.tags.Lud16Tag @@ -51,3 +52,5 @@ fun TagArrayBuilder.lud06(lud06: String) = addUnique(Lud06Tag.ass fun TagArrayBuilder.banner(banner: String) = addUnique(BannerTag.assemble(banner)) fun TagArrayBuilder.pronouns(pronouns: String) = addUnique(PronounsTag.assemble(pronouns)) + +fun TagArrayBuilder.clinkOffer(offer: String) = addUnique(ClinkOfferTag.assemble(offer)) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/tags/ClinkOfferTag.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/tags/ClinkOfferTag.kt new file mode 100644 index 0000000000..a744501813 --- /dev/null +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/tags/ClinkOfferTag.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.metadata.tags + +import com.vitorpamplona.quartz.utils.ensure + +/** CLINK Offers pointer (`noffer1…`) advertised in a kind-0 profile, mirroring the NIP-05 `clink_offer` key. */ +class ClinkOfferTag { + companion object { + const val TAG_NAME = "clink_offer" + + 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(offer: String) = arrayOf(TAG_NAME, offer) + } +} diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt index 5470364ffa..86aa586878 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/nip01Core/metadata/UpdateMetadataTest.kt @@ -205,7 +205,10 @@ class UpdateMetadataTest { // written into kind-0 content under the spec's `clink_offer` key assertEquals(true, event.content.contains("\"clink_offer\":\"$noffer\"")) - // and parses back out + // and dual-written as a kind-0 tag (NIP-1770 pattern), like the other fields + assertContentEquals(arrayOf("clink_offer", noffer), event.tags.firstOrNull { it.firstOrNull() == "clink_offer" }) + + // and parses back out of the content val metadata = event.contactMetaData() assertNotNull(metadata) assertEquals(noffer, metadata.clinkOffer)