NIP-30: fix EmojiPackEvent.build typing and add metadata helpers

The build DSL was typed as TagArrayBuilder<GitRepositoryEvent>, which made
signer.sign(template) return the wrong event subclass. Retype it to
EmojiPackEvent and add local TagArrayBuilder extensions for title,
description, and image tags. Also add title()/description()/image()
accessors on EmojiPackEvent to match the LabeledBookmarkListEvent pattern.
This commit is contained in:
Claude
2026-04-20 17:57:42 +00:00
parent f76e4f4c49
commit 5d74b4175b
3 changed files with 143 additions and 5 deletions
@@ -26,9 +26,11 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip31Alts.alt
import com.vitorpamplona.quartz.nip34Git.repository.GitRepositoryEvent
import com.vitorpamplona.quartz.nip34Git.repository.name
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.NameTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
@@ -42,6 +44,18 @@ class EmojiPackEvent(
content: String,
sig: HexKey,
) : PrivateTagArrayEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
@Deprecated("NIP-51 has deprecated name. Use title instead", ReplaceWith("title()"))
fun name() = tags.firstNotNullOfOrNull(NameTag::parse)
fun title() = tags.firstNotNullOfOrNull(TitleTag::parse)
@Suppress("DEPRECATION")
fun titleOrName() = title() ?: name()
fun description() = tags.firstNotNullOfOrNull(DescriptionTag::parse)
fun image() = tags.firstNotNullOfOrNull(ImageTag::parse)
companion object {
const val KIND = 30030
const val ALT_DESCRIPTION = "Emoji pack"
@@ -51,11 +65,11 @@ class EmojiPackEvent(
name: String,
dTag: String = Uuid.random().toString(),
createdAt: Long = TimeUtils.now(),
initializer: TagArrayBuilder<GitRepositoryEvent>.() -> Unit = {},
) = eventTemplate(KIND, "", createdAt) {
initializer: TagArrayBuilder<EmojiPackEvent>.() -> Unit = {},
) = eventTemplate<EmojiPackEvent>(KIND, "", createdAt) {
alt(ALT_DESCRIPTION)
dTag(dTag)
name(name)
title(name)
initializer()
}
}
@@ -0,0 +1,32 @@
/*
* 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.nip30CustomEmoji.pack
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip51Lists.tags.DescriptionTag
import com.vitorpamplona.quartz.nip51Lists.tags.ImageTag
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
fun TagArrayBuilder<EmojiPackEvent>.title(title: String) = addUnique(TitleTag.assemble(title))
fun TagArrayBuilder<EmojiPackEvent>.description(listDescription: String) = addUnique(DescriptionTag.assemble(listDescription))
fun TagArrayBuilder<EmojiPackEvent>.image(url: String) = addUnique(ImageTag.assemble(url))
@@ -0,0 +1,92 @@
/*
* 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.nip30CustomEmoji
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.description
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.image
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class EmojiPackEventBuildTest {
@Test
fun buildIncludesTitleDTagAltAndEmojis() {
val template =
EmojiPackEvent.build(name = "My Pack", dTag = "my-pack") {
description("A test pack")
image("https://example.com/cover.jpg")
emoji("smile", "https://example.com/smile.png")
}
assertEquals(EmojiPackEvent.KIND, template.kind)
assertEquals("", template.content)
val tagsByName = template.tags.groupBy { it[0] }
assertTrue(tagsByName.containsKey("d"))
assertEquals("my-pack", tagsByName["d"]!!.first()[1])
assertTrue(tagsByName.containsKey("title"))
assertEquals("My Pack", tagsByName["title"]!!.first()[1])
assertTrue(tagsByName.containsKey("description"))
assertEquals("A test pack", tagsByName["description"]!!.first()[1])
assertTrue(tagsByName.containsKey("image"))
assertEquals("https://example.com/cover.jpg", tagsByName["image"]!!.first()[1])
assertTrue(tagsByName.containsKey("alt"))
assertTrue(tagsByName.containsKey("emoji"))
assertEquals("smile", tagsByName["emoji"]!!.first()[1])
assertEquals("https://example.com/smile.png", tagsByName["emoji"]!!.first()[2])
}
@Test
fun emojiTagUsesEventTypedBuilder() {
// Regression: EmojiPackEvent.build previously used TagArrayBuilder<GitRepositoryEvent>,
// which caused callers to get a template of the wrong type. This test locks in the fix.
val template = EmojiPackEvent.build(name = "test")
assertEquals(EmojiPackEvent.KIND, template.kind)
}
@Test
fun titleDescriptionImageAccessorsReadTags() {
val event =
EmojiPackEvent(
id = "00",
pubKey = "00",
createdAt = 0L,
tags =
arrayOf(
arrayOf("d", "mypack"),
arrayOf("title", "My Pack"),
arrayOf("description", "desc"),
arrayOf("image", "https://example.com/cover.png"),
arrayOf("emoji", "smile", "https://example.com/smile.png"),
),
content = "",
sig = "00",
)
assertEquals("My Pack", event.title())
assertEquals("desc", event.description())
assertEquals("https://example.com/cover.png", event.image())
assertEquals("My Pack", event.titleOrName())
assertEquals(1, event.taggedEmojis().size)
assertEquals("smile", event.taggedEmojis().first().code)
}
}