feat: implement NIP-32 Labeling protocol in quartz

Add kind 1985 LabelEvent with L (namespace) and l (label) tag support
for distributed moderation, content classification, and license assignment.
Includes tag parsers, builder extensions, self-reporting helpers, and tests.

https://claude.ai/code/session_011t5ZoP1BdgZTT5Cen9GH5z
This commit is contained in:
Claude
2026-03-28 03:57:46 +00:00
parent 007a0b0d43
commit aa15085f40
9 changed files with 730 additions and 1 deletions
+1 -1
View File
@@ -71,7 +71,7 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
- [ ] Relay-based Groups (NIP-29)
- [x] Custom Emoji (NIP-30)
- [x] Event alt descriptors (NIP-31)
- [ ] Labeling (NIP-32)
- [x] Labeling (NIP-32)
- [x] Git Stuff (NIP-34)
- [x] Torrents (NIP-35)
- [x] Sensitive Content (NIP-36)
@@ -0,0 +1,33 @@
/*
* 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.nip32Labeling
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip32Labeling.tags.LabelNamespaceTag
import com.vitorpamplona.quartz.nip32Labeling.tags.LabelTag
/**
* NIP-32 self-reporting: `l` and `L` tags MAY be added to any event kind.
* For non-1985 events, labels refer to the event itself.
*/
fun Event.selfReportLabels() = tags.mapNotNull(LabelTag::parse)
fun Event.selfReportNamespaces() = tags.mapNotNull(LabelNamespaceTag::parse)
@@ -0,0 +1,153 @@
/*
* 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.nip32Labeling
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHint
import com.vitorpamplona.quartz.nip01Core.hints.types.PubKeyHint
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip01Core.tags.events.eTag
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.HashtagTag
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
import com.vitorpamplona.quartz.nip01Core.tags.people.pTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip32Labeling.tags.LabelNamespaceTag
import com.vitorpamplona.quartz.nip32Labeling.tags.LabelTag
import com.vitorpamplona.quartz.utils.TimeUtils
/**
* NIP-32: Label Event (kind 1985)
*
* Attaches labels to existing events, pubkeys, relays, or topics.
* Supports distributed moderation, collection management, license assignment,
* and content classification.
*/
@Immutable
class LabelEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: Array<Array<String>>,
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
EventHintProvider,
PubKeyHintProvider,
AddressHintProvider {
override fun pubKeyHints(): List<PubKeyHint> = tags.mapNotNull(PTag::parseAsHint)
override fun linkedPubKeys(): List<HexKey> = tags.mapNotNull(PTag::parseKey)
override fun eventHints(): List<EventIdHint> = tags.mapNotNull(ETag::parseAsHint)
override fun linkedEventIds(): List<HexKey> = tags.mapNotNull(ETag::parseId)
override fun addressHints(): List<AddressHint> = tags.mapNotNull(ATag::parseAsHint)
override fun linkedAddressIds(): List<String> = tags.mapNotNull(ATag::parseAddressId)
/** All label namespace (`L`) tags on this event. */
fun namespaces() = tags.mapNotNull(LabelNamespaceTag::parse)
/** All label (`l`) tags on this event. */
fun labels() = tags.mapNotNull(LabelTag::parse)
/** Labels filtered by a specific namespace. */
fun labelsByNamespace(namespace: String) = labels().filter { it.namespace == namespace }
/** Referenced event IDs (label targets). */
fun labeledEvents() = tags.mapNotNull(ETag::parseId)
/** Referenced pubkeys (label targets). */
fun labeledPubKeys() = tags.mapNotNull(PTag::parseKey)
/** Referenced addresses (label targets). */
fun labeledAddresses() = tags.mapNotNull(ATag::parseAddressId)
/** Referenced hashtags/topics (label targets via `t` tag). */
fun labeledHashtags() = tags.mapNotNull(HashtagTag::parse)
/** Referenced relay URLs (label targets via `r` tag). */
fun labeledRelayUrls(): List<String> =
tags
.filter { it.size >= 2 && it[0] == "r" && it[1].isNotEmpty() }
.map { it[1] }
companion object {
const val KIND = 1985
const val ALT = "Label event"
/**
* Build a label event for labeling events.
*/
fun buildEventLabel(
labeledEventId: HexKey,
labeledEventRelay: String? = null,
labeledEventAuthor: HexKey? = null,
labels: List<LabelTag>,
content: String = "",
createdAt: Long = TimeUtils.now(),
) = eventTemplate<LabelEvent>(KIND, content, createdAt) {
alt(ALT)
eTag(ETag(labeledEventId, labeledEventRelay?.let { RelayUrlNormalizer.normalizeOrNull(it) }, labeledEventAuthor))
labels.map { it.namespace }.distinct().forEach { labelNamespace(it) }
labels.forEach { label(it) }
}
/**
* Build a label event for labeling pubkeys.
*/
fun buildPubKeyLabel(
labeledPubKey: HexKey,
labeledPubKeyRelay: String? = null,
labels: List<LabelTag>,
content: String = "",
createdAt: Long = TimeUtils.now(),
) = eventTemplate<LabelEvent>(KIND, content, createdAt) {
alt(ALT)
pTag(labeledPubKey, labeledPubKeyRelay?.let { RelayUrlNormalizer.normalizeOrNull(it) })
labels.map { it.namespace }.distinct().forEach { labelNamespace(it) }
labels.forEach { label(it) }
}
/**
* Build a label event with custom tag targets and labels.
*/
fun build(
labels: List<LabelTag>,
content: String = "",
createdAt: Long = TimeUtils.now(),
) = eventTemplate<LabelEvent>(KIND, content, createdAt) {
alt(ALT)
labels.map { it.namespace }.distinct().forEach { labelNamespace(it) }
labels.forEach { label(it) }
}
}
}
@@ -0,0 +1,41 @@
/*
* 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.nip32Labeling
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip32Labeling.tags.LabelNamespaceTag
import com.vitorpamplona.quartz.nip32Labeling.tags.LabelTag
/** Adds a label (`l`) tag. */
fun <T : Event> TagArrayBuilder<T>.label(tag: LabelTag) = add(tag.toTagArray())
/** Adds a label (`l`) tag with explicit label and namespace values. */
fun <T : Event> TagArrayBuilder<T>.label(
label: String,
namespace: String,
) = add(LabelTag.assemble(label, namespace))
/** Adds a label namespace (`L`) tag. */
fun <T : Event> TagArrayBuilder<T>.labelNamespace(namespace: String) = add(LabelNamespaceTag.assemble(namespace))
/** Adds a label namespace (`L`) tag. */
fun <T : Event> TagArrayBuilder<T>.labelNamespace(tag: LabelNamespaceTag) = add(tag.toTagArray())
@@ -0,0 +1,77 @@
/*
* 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.nip32Labeling.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
/**
* NIP-32: `L` tag label namespace.
*
* Format: `["L", "<namespace>"]`
*
* Namespaces SHOULD be unambiguous (ISO standard or reverse domain name notation).
* The special `ugc` namespace MAY be used when label content is provided by an end user.
* `L` tags starting with `#` indicate that the label target should be associated
* with the label's value (attaching standard nostr tags to events, pubkeys, etc.).
*/
@Immutable
data class LabelNamespaceTag(
val namespace: String,
) {
fun toTagArray() = assemble(namespace)
/**
* Returns true if this namespace is a tag-association namespace (starts with `#`).
* When `L` = `#t`, an `l` tag like `["l", "bitcoin", "#t"]` means the target
* should be associated with the hashtag `bitcoin`.
*/
fun isTagAssociation() = namespace.startsWith("#")
companion object {
const val TAG_NAME = "L"
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
fun isTagged(
tag: Array<String>,
namespace: String,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == namespace
fun parse(tag: Array<String>): LabelNamespaceTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return LabelNamespaceTag(tag[1])
}
fun parseNamespace(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(namespace: String) = arrayOf(TAG_NAME, namespace)
}
}
@@ -0,0 +1,78 @@
/*
* 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.nip32Labeling.tags
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.nip01Core.core.has
import com.vitorpamplona.quartz.utils.ensure
/**
* NIP-32: `l` tag label value with an optional namespace mark.
*
* Format: `["l", "<label>", "<namespace>"]`
*
* If no namespace mark is included, `ugc` is implied.
*/
@Immutable
data class LabelTag(
val label: String,
val namespace: String,
) {
fun toTagArray() = assemble(label, namespace)
companion object {
const val TAG_NAME = "l"
const val DEFAULT_NAMESPACE = "ugc"
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
fun isTagged(
tag: Array<String>,
label: String,
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == label
fun isTaggedWithNamespace(
tag: Array<String>,
namespace: String,
) = tag.has(2) && tag[0] == TAG_NAME && tag[2] == namespace
fun parse(tag: Array<String>): LabelTag? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
val namespace = if (tag.has(2) && tag[2].isNotEmpty()) tag[2] else DEFAULT_NAMESPACE
return LabelTag(tag[1], namespace)
}
fun parseLabel(tag: Array<String>): String? {
ensure(tag.has(1)) { return null }
ensure(tag[0] == TAG_NAME) { return null }
ensure(tag[1].isNotEmpty()) { return null }
return tag[1]
}
fun assemble(
label: String,
namespace: String,
) = arrayOf(TAG_NAME, label, namespace)
}
}
@@ -68,6 +68,7 @@ import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
import com.vitorpamplona.quartz.nip28PublicChat.message.ChannelMessageEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.selection.EmojiPackSelectionEvent
import com.vitorpamplona.quartz.nip32Labeling.LabelEvent
import com.vitorpamplona.quartz.nip34Git.issue.GitIssueEvent
import com.vitorpamplona.quartz.nip34Git.patch.GitPatchEvent
import com.vitorpamplona.quartz.nip34Git.reply.GitReplyEvent
@@ -261,6 +262,7 @@ class EventFactory {
InteractiveStoryPrologueEvent.KIND -> InteractiveStoryPrologueEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStorySceneEvent.KIND -> InteractiveStorySceneEvent(id, pubKey, createdAt, tags, content, sig)
InteractiveStoryReadingStateEvent.KIND -> InteractiveStoryReadingStateEvent(id, pubKey, createdAt, tags, content, sig)
LabelEvent.KIND -> LabelEvent(id, pubKey, createdAt, tags, content, sig)
LabeledBookmarkListEvent.KIND -> LabeledBookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesChatMessageEvent.KIND -> LiveActivitiesChatMessageEvent(id, pubKey, createdAt, tags, content, sig)
LiveActivitiesEvent.KIND -> LiveActivitiesEvent(id, pubKey, createdAt, tags, content, sig)
@@ -0,0 +1,189 @@
/*
* 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.nip32Labeling
import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip32Labeling.tags.LabelTag
import com.vitorpamplona.quartz.utils.EventFactory
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertTrue
class LabelEventTest {
private val signer = NostrSignerInternal(KeyPair())
private val testEventId = "a".repeat(64)
private val testPubKey = "b".repeat(64)
@Test
fun testBuildEventLabel() {
val labels = listOf(LabelTag("approve", "nip28.moderation"))
val template =
LabelEvent.buildEventLabel(
labeledEventId = testEventId,
labels = labels,
content = "Approved for channel",
)
assertEquals(1985, template.kind)
assertEquals("Approved for channel", template.content)
val hasLabelNamespace = template.tags.any { it[0] == "L" && it[1] == "nip28.moderation" }
assertTrue(hasLabelNamespace, "Should have L namespace tag")
val hasLabel = template.tags.any { it[0] == "l" && it[1] == "approve" && it[2] == "nip28.moderation" }
assertTrue(hasLabel, "Should have l label tag")
val hasEventRef = template.tags.any { it[0] == "e" && it[1] == testEventId }
assertTrue(hasEventRef, "Should have e tag targeting labeled event")
}
@Test
fun testBuildPubKeyLabel() {
val labels = listOf(LabelTag("permies", "#t"))
val template =
LabelEvent.buildPubKeyLabel(
labeledPubKey = testPubKey,
labels = labels,
content = "",
)
assertEquals(1985, template.kind)
val hasPubKeyRef = template.tags.any { it[0] == "p" && it[1] == testPubKey }
assertTrue(hasPubKeyRef, "Should have p tag targeting labeled pubkey")
val hasNamespace = template.tags.any { it[0] == "L" && it[1] == "#t" }
assertTrue(hasNamespace, "Should have L namespace tag")
val hasLabel = template.tags.any { it[0] == "l" && it[1] == "permies" && it[2] == "#t" }
assertTrue(hasLabel, "Should have l label tag")
}
@Test
fun testBuildMultipleLabels() {
val labels =
listOf(
LabelTag("IT-MI", "ISO-3166-2"),
LabelTag("en", "ISO-639-1"),
)
val template = LabelEvent.build(labels = labels)
val namespaces =
template.tags
.filter { it[0] == "L" }
.map { it[1] }
.toSet()
assertEquals(setOf("ISO-3166-2", "ISO-639-1"), namespaces)
val labelTags = template.tags.filter { it[0] == "l" }
assertEquals(2, labelTags.size)
}
@Test
fun testEventFactoryCreatesLabelEvent() {
val event =
EventFactory.create<Event>(
id = testEventId,
pubKey = testPubKey,
createdAt = 1234567890L,
kind = 1985,
tags =
arrayOf(
arrayOf("L", "license"),
arrayOf("l", "MIT", "license"),
arrayOf("e", testEventId),
),
content = "",
sig = "c".repeat(128),
)
assertIs<LabelEvent>(event)
assertEquals(1985, event.kind)
val labels = event.labels()
assertEquals(1, labels.size)
assertEquals("MIT", labels[0].label)
assertEquals("license", labels[0].namespace)
val namespaces = event.namespaces()
assertEquals(1, namespaces.size)
assertEquals("license", namespaces[0].namespace)
}
@Test
fun testLabelsByNamespace() {
val event =
EventFactory.create<LabelEvent>(
id = testEventId,
pubKey = testPubKey,
createdAt = 1234567890L,
kind = 1985,
tags =
arrayOf(
arrayOf("L", "license"),
arrayOf("L", "ISO-639-1"),
arrayOf("l", "MIT", "license"),
arrayOf("l", "en", "ISO-639-1"),
arrayOf("e", testEventId),
),
content = "",
sig = "c".repeat(128),
)
val licenseLabels = event.labelsByNamespace("license")
assertEquals(1, licenseLabels.size)
assertEquals("MIT", licenseLabels[0].label)
val languageLabels = event.labelsByNamespace("ISO-639-1")
assertEquals(1, languageLabels.size)
assertEquals("en", languageLabels[0].label)
}
@Test
fun testLabeledTargets() {
val event =
EventFactory.create<LabelEvent>(
id = testEventId,
pubKey = testPubKey,
createdAt = 1234567890L,
kind = 1985,
tags =
arrayOf(
arrayOf("L", "ugc"),
arrayOf("l", "funny", "ugc"),
arrayOf("e", testEventId),
arrayOf("p", testPubKey),
arrayOf("t", "bitcoin"),
arrayOf("r", "wss://relay.example.com"),
),
content = "",
sig = "c".repeat(128),
)
assertEquals(listOf(testEventId), event.labeledEvents())
assertEquals(listOf(testPubKey), event.labeledPubKeys())
assertEquals(listOf("bitcoin"), event.labeledHashtags())
assertEquals(listOf("wss://relay.example.com"), event.labeledRelayUrls())
}
}
@@ -0,0 +1,156 @@
/*
* 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.nip32Labeling
import com.vitorpamplona.quartz.nip32Labeling.tags.LabelNamespaceTag
import com.vitorpamplona.quartz.nip32Labeling.tags.LabelTag
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
class LabelTagTest {
@Test
fun testParseLabelWithNamespace() {
val tag = arrayOf("l", "MIT", "license")
val label = LabelTag.parse(tag)
assertNotNull(label)
assertEquals("MIT", label.label)
assertEquals("license", label.namespace)
}
@Test
fun testParseLabelWithoutNamespaceDefaultsToUgc() {
val tag = arrayOf("l", "my-label")
val label = LabelTag.parse(tag)
assertNotNull(label)
assertEquals("my-label", label.label)
assertEquals("ugc", label.namespace)
}
@Test
fun testParseLabelWithEmptyNamespaceDefaultsToUgc() {
val tag = arrayOf("l", "my-label", "")
val label = LabelTag.parse(tag)
assertNotNull(label)
assertEquals("my-label", label.label)
assertEquals("ugc", label.namespace)
}
@Test
fun testParseLabelReturnsNullForInvalidTag() {
assertNull(LabelTag.parse(arrayOf("l")))
assertNull(LabelTag.parse(arrayOf("l", "")))
assertNull(LabelTag.parse(arrayOf("e", "something")))
assertNull(LabelTag.parse(arrayOf()))
}
@Test
fun testParseLabelValue() {
val tag = arrayOf("l", "en", "ISO-639-1")
assertEquals("en", LabelTag.parseLabel(tag))
}
@Test
fun testAssembleLabel() {
val assembled = LabelTag.assemble("MIT", "license")
assertEquals(3, assembled.size)
assertEquals("l", assembled[0])
assertEquals("MIT", assembled[1])
assertEquals("license", assembled[2])
}
@Test
fun testRoundTrip() {
val original = LabelTag("approve", "nip28.moderation")
val assembled = original.toTagArray()
val parsed = LabelTag.parse(assembled)
assertNotNull(parsed)
assertEquals(original, parsed)
}
@Test
fun testIsTagged() {
assertTrue(LabelTag.isTagged(arrayOf("l", "something")))
assertTrue(LabelTag.isTagged(arrayOf("l", "MIT", "license")))
}
@Test
fun testIsTaggedWithValue() {
assertTrue(LabelTag.isTagged(arrayOf("l", "MIT", "license"), "MIT"))
}
@Test
fun testIsTaggedWithNamespace() {
assertTrue(LabelTag.isTaggedWithNamespace(arrayOf("l", "MIT", "license"), "license"))
}
@Test
fun testParseNamespace() {
val tag = arrayOf("L", "license")
val namespace = LabelNamespaceTag.parse(tag)
assertNotNull(namespace)
assertEquals("license", namespace.namespace)
}
@Test
fun testParseNamespaceReturnsNullForInvalid() {
assertNull(LabelNamespaceTag.parse(arrayOf("L")))
assertNull(LabelNamespaceTag.parse(arrayOf("L", "")))
assertNull(LabelNamespaceTag.parse(arrayOf("l", "something")))
}
@Test
fun testAssembleNamespace() {
val assembled = LabelNamespaceTag.assemble("com.example.ontology")
assertEquals(2, assembled.size)
assertEquals("L", assembled[0])
assertEquals("com.example.ontology", assembled[1])
}
@Test
fun testNamespaceRoundTrip() {
val original = LabelNamespaceTag("ISO-639-1")
val assembled = original.toTagArray()
val parsed = LabelNamespaceTag.parse(assembled)
assertNotNull(parsed)
assertEquals(original, parsed)
}
@Test
fun testTagAssociationNamespace() {
val ns = LabelNamespaceTag("#t")
assertTrue(ns.isTagAssociation())
}
@Test
fun testParseNamespaceValue() {
assertEquals("ugc", LabelNamespaceTag.parseNamespace(arrayOf("L", "ugc")))
assertNull(LabelNamespaceTag.parseNamespace(arrayOf("l", "ugc")))
}
}