refactor: align ContactCardEvent with the nip88Polls class structure

Replicates the PollEvent layout: all tag parsing moves to TagArray extensions
in TagArrayExt.kt with the event accessors delegating to them, builder
extensions use addUnique for single-instance tags, and construction goes
through template-returning builders instead of methods that sign internally.

- build() returns an EventTemplate<ContactCardEvent> (the signer is only used
  to NIP-44 encrypt the private tags, matching TrustProviderListEvent);
  create() remains as the signer.sign(build(...)) convenience and now takes
  the emoji list directly
- updatePetNameAndSummary() returns an unsigned EventTemplate; callers sign
  it (ContactCardsState and tests updated)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QdvE4LvgkSewJXyFzyyAUY
This commit is contained in:
Claude
2026-07-13 12:28:23 +00:00
parent ebeecd1ee7
commit 184f0bfde7
5 changed files with 167 additions and 103 deletions
@@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -116,20 +115,22 @@ class ContactCardsState(
): ContactCardEvent {
val existing = getCard(target)
return if (existing != null) {
ContactCardEvent.updatePetNameAndSummary(
earlierVersion = existing,
petName = petName,
summary = summary,
emojis = emojis,
signer = signer,
signer.sign(
ContactCardEvent.updatePetNameAndSummary(
earlierVersion = existing,
petName = petName,
summary = summary,
emojis = emojis,
signer = signer,
),
)
} else {
ContactCardEvent.create(
targetUser = target,
petName = petName,
summary = summary,
emojis = emojis,
signer = signer,
privateInitializer = { emojis(emojis) },
)
}
}
@@ -25,34 +25,20 @@ import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
import com.vitorpamplona.quartz.nip01Core.core.tagArray
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip50Search.SearchableEvent
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEvent
import com.vitorpamplona.quartz.nip51Lists.encryption.PrivateTagsInContent
import com.vitorpamplona.quartz.nip51Lists.remove
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ActiveHoursEndTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ActiveHoursStartTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.FirstCreatedAtTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.FollowerCountTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.PetNameTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.PostCountTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.RankTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ReactionsCountTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ReplyCountTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ReportsCountReceivedTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ReportsCountSentTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.SummaryTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.TopicTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapAmountReceivedTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapAmountSentTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapAvgAmountDayReceivedTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapAvgAmountDaySentTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapCountReceivedTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapCountSentTag
import com.vitorpamplona.quartz.utils.TimeUtils
@Immutable
@@ -71,43 +57,43 @@ class ContactCardEvent(
fun aboutUser() = tags.dTag()
fun rank() = tags.firstNotNullOfOrNull(RankTag::parse)
fun rank() = tags.rank()
fun followerCount() = tags.firstNotNullOfOrNull(FollowerCountTag::parse)
fun followerCount() = tags.followerCount()
fun firstCreatedAt() = tags.firstNotNullOfOrNull(FirstCreatedAtTag::parse)
fun firstCreatedAt() = tags.firstCreatedAt()
fun postCount() = tags.firstNotNullOfOrNull(PostCountTag::parse)
fun postCount() = tags.postCount()
fun replyCount() = tags.firstNotNullOfOrNull(ReplyCountTag::parse)
fun replyCount() = tags.replyCount()
fun reactionsCount() = tags.firstNotNullOfOrNull(ReactionsCountTag::parse)
fun reactionsCount() = tags.reactionsCount()
fun zapAmountReceived() = tags.firstNotNullOfOrNull(ZapAmountReceivedTag::parse)
fun zapAmountReceived() = tags.zapAmountReceived()
fun zapAmountSent() = tags.firstNotNullOfOrNull(ZapAmountSentTag::parse)
fun zapAmountSent() = tags.zapAmountSent()
fun zapCountReceived() = tags.firstNotNullOfOrNull(ZapCountReceivedTag::parse)
fun zapCountReceived() = tags.zapCountReceived()
fun zapCountSent() = tags.firstNotNullOfOrNull(ZapCountSentTag::parse)
fun zapCountSent() = tags.zapCountSent()
fun zapAvgAmountDayReceived() = tags.firstNotNullOfOrNull(ZapAvgAmountDayReceivedTag::parse)
fun zapAvgAmountDayReceived() = tags.zapAvgAmountDayReceived()
fun zapAvgAmountDaySent() = tags.firstNotNullOfOrNull(ZapAvgAmountDaySentTag::parse)
fun zapAvgAmountDaySent() = tags.zapAvgAmountDaySent()
fun reportsCountReceived() = tags.firstNotNullOfOrNull(ReportsCountReceivedTag::parse)
fun reportsCountReceived() = tags.reportsCountReceived()
fun reportsCountSent() = tags.firstNotNullOfOrNull(ReportsCountSentTag::parse)
fun reportsCountSent() = tags.reportsCountSent()
fun topics() = tags.mapNotNull(TopicTag::parse)
fun topics() = tags.topics()
fun activeHoursStart() = tags.firstNotNullOfOrNull(ActiveHoursStartTag::parse)
fun activeHoursStart() = tags.activeHoursStart()
fun activeHoursEnd() = tags.firstNotNullOfOrNull(ActiveHoursEndTag::parse)
fun activeHoursEnd() = tags.activeHoursEnd()
fun petName() = tags.firstNotNullOfOrNull(PetNameTag::parse)
fun petName() = tags.petName()
fun summary() = tags.firstNotNullOfOrNull(SummaryTag::parse)
fun summary() = tags.summary()
companion object {
const val KIND = 30382
@@ -126,35 +112,54 @@ class ContactCardEvent(
targetUser: HexKey,
petName: String? = null,
summary: String? = null,
emojis: List<EmojiUrlTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
publicInitializer: TagArrayBuilder<ContactCardEvent>.() -> Unit = {},
privateInitializer: TagArrayBuilder<ContactCardEvent>.() -> Unit = {},
): ContactCardEvent {
val publicTags =
tagArray {
dTag(targetUser)
publicInitializer()
}
): ContactCardEvent = signer.sign(build(targetUser, petName, summary, emojis, signer, createdAt, publicInitializer, privateInitializer))
/**
* Unsigned template for a new card about [targetUser]. The petname, summary
* and the NIP-30 emoji mappings their shortcodes use always go in the NIP-44
* encrypted content ([signer] only encrypts here; the caller signs).
*/
suspend fun build(
targetUser: HexKey,
petName: String? = null,
summary: String? = null,
emojis: List<EmojiUrlTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
publicInitializer: TagArrayBuilder<ContactCardEvent>.() -> Unit = {},
privateInitializer: TagArrayBuilder<ContactCardEvent>.() -> Unit = {},
): EventTemplate<ContactCardEvent> {
val privateTags =
tagArray {
petName?.let { petName(it) }
summary?.let { summary(it) }
emojis(emojis)
privateInitializer()
}
val encryptedContent = PrivateTagsInContent.encryptNip44(privateTags, signer)
return signer.sign(createdAt, KIND, publicTags, encryptedContent)
return eventTemplate(
kind = KIND,
description = PrivateTagsInContent.encryptNip44(privateTags, signer),
createdAt = createdAt,
) {
dTag(targetUser)
publicInitializer()
}
}
/**
* Replaces the petname, summary and their NIP-30 custom emoji mappings on an
* existing card, keeping every other public and private tag intact. All of
* them always live in the NIP-44 encrypted content — any stray public
* petname/summary copy is stripped. A `null` value removes the field; the
* private `emoji` tag set is replaced wholesale since it only exists to
* render the petname/summary shortcodes.
* Unsigned template that replaces the petname, summary and their NIP-30
* custom emoji mappings on an existing card, keeping every other public and
* private tag intact. All of them always live in the NIP-44 encrypted
* content — any stray public petname/summary copy is stripped. A `null`
* value removes the field; the private `emoji` tag set is replaced
* wholesale since it only exists to render the petname/summary shortcodes.
* [signer] only decrypts/encrypts here; the caller signs the template.
*/
suspend fun updatePetNameAndSummary(
earlierVersion: ContactCardEvent,
@@ -163,7 +168,7 @@ class ContactCardEvent(
emojis: List<EmojiUrlTag> = emptyList(),
signer: NostrSigner,
createdAt: Long = TimeUtils.now(),
): ContactCardEvent {
): EventTemplate<ContactCardEvent> {
val privateTags =
earlierVersion.privateTags(signer)
?: throw SignerExceptions.UnauthorizedDecryptionException()
@@ -185,11 +190,11 @@ class ContactCardEvent(
.remove(arrayOf(PetNameTag.TAG_NAME))
.remove(arrayOf(SummaryTag.TAG_NAME))
return signer.sign(
createdAt,
KIND,
newPublicTags,
PrivateTagsInContent.encryptNip44(newPrivateTags, signer),
return EventTemplate(
createdAt = createdAt,
kind = KIND,
tags = newPublicTags,
content = PrivateTagsInContent.encryptNip44(newPrivateTags, signer),
)
}
}
@@ -41,40 +41,40 @@ import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapAvgAmountDa
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapCountReceivedTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapCountSentTag
fun TagArrayBuilder<ContactCardEvent>.rank(rank: Int) = add(RankTag.assemble(rank))
fun TagArrayBuilder<ContactCardEvent>.rank(rank: Int) = addUnique(RankTag.assemble(rank))
fun TagArrayBuilder<ContactCardEvent>.followers(count: Int) = add(FollowerCountTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.followers(count: Int) = addUnique(FollowerCountTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.firstCreatedAt(timestamp: Long) = add(FirstCreatedAtTag.assemble(timestamp))
fun TagArrayBuilder<ContactCardEvent>.firstCreatedAt(timestamp: Long) = addUnique(FirstCreatedAtTag.assemble(timestamp))
fun TagArrayBuilder<ContactCardEvent>.postCount(count: Int) = add(PostCountTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.postCount(count: Int) = addUnique(PostCountTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.replyCount(count: Int) = add(ReplyCountTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.replyCount(count: Int) = addUnique(ReplyCountTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.reactionsCount(count: Int) = add(ReactionsCountTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.reactionsCount(count: Int) = addUnique(ReactionsCountTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.zapAmountReceived(sats: Long) = add(ZapAmountReceivedTag.assemble(sats))
fun TagArrayBuilder<ContactCardEvent>.zapAmountReceived(sats: Long) = addUnique(ZapAmountReceivedTag.assemble(sats))
fun TagArrayBuilder<ContactCardEvent>.zapAmountSent(sats: Long) = add(ZapAmountSentTag.assemble(sats))
fun TagArrayBuilder<ContactCardEvent>.zapAmountSent(sats: Long) = addUnique(ZapAmountSentTag.assemble(sats))
fun TagArrayBuilder<ContactCardEvent>.zapCountReceived(count: Int) = add(ZapCountReceivedTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.zapCountReceived(count: Int) = addUnique(ZapCountReceivedTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.zapCountSent(count: Int) = add(ZapCountSentTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.zapCountSent(count: Int) = addUnique(ZapCountSentTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.zapAvgAmountDayReceived(sats: Long) = add(ZapAvgAmountDayReceivedTag.assemble(sats))
fun TagArrayBuilder<ContactCardEvent>.zapAvgAmountDayReceived(sats: Long) = addUnique(ZapAvgAmountDayReceivedTag.assemble(sats))
fun TagArrayBuilder<ContactCardEvent>.zapAvgAmountDaySent(sats: Long) = add(ZapAvgAmountDaySentTag.assemble(sats))
fun TagArrayBuilder<ContactCardEvent>.zapAvgAmountDaySent(sats: Long) = addUnique(ZapAvgAmountDaySentTag.assemble(sats))
fun TagArrayBuilder<ContactCardEvent>.reportsCountReceived(count: Int) = add(ReportsCountReceivedTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.reportsCountReceived(count: Int) = addUnique(ReportsCountReceivedTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.reportsCountSent(count: Int) = add(ReportsCountSentTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.reportsCountSent(count: Int) = addUnique(ReportsCountSentTag.assemble(count))
fun TagArrayBuilder<ContactCardEvent>.topic(topic: String) = add(TopicTag.assemble(topic))
fun TagArrayBuilder<ContactCardEvent>.activeHoursStart(hour: Int) = add(ActiveHoursStartTag.assemble(hour))
fun TagArrayBuilder<ContactCardEvent>.activeHoursStart(hour: Int) = addUnique(ActiveHoursStartTag.assemble(hour))
fun TagArrayBuilder<ContactCardEvent>.activeHoursEnd(hour: Int) = add(ActiveHoursEndTag.assemble(hour))
fun TagArrayBuilder<ContactCardEvent>.activeHoursEnd(hour: Int) = addUnique(ActiveHoursEndTag.assemble(hour))
fun TagArrayBuilder<ContactCardEvent>.petName(name: String) = add(PetNameTag.assemble(name))
fun TagArrayBuilder<ContactCardEvent>.petName(name: String) = addUnique(PetNameTag.assemble(name))
fun TagArrayBuilder<ContactCardEvent>.summary(summary: String) = add(SummaryTag.assemble(summary))
fun TagArrayBuilder<ContactCardEvent>.summary(summary: String) = addUnique(SummaryTag.assemble(summary))
@@ -22,8 +22,59 @@ package com.vitorpamplona.quartz.nip85TrustedAssertions.users
import com.vitorpamplona.quartz.nip01Core.core.TagArray
import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ActiveHoursEndTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ActiveHoursStartTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.FirstCreatedAtTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.FollowerCountTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.PetNameTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.PostCountTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.RankTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ReactionsCountTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ReplyCountTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ReportsCountReceivedTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ReportsCountSentTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.SummaryTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.TopicTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapAmountReceivedTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapAmountSentTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapAvgAmountDayReceivedTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapAvgAmountDaySentTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapCountReceivedTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.ZapCountSentTag
fun TagArray.rank() = fastFirstNotNullOfOrNull(RankTag::parse)
fun TagArray.followerCount() = fastFirstNotNullOfOrNull(FollowerCountTag::parse)
fun TagArray.firstCreatedAt() = fastFirstNotNullOfOrNull(FirstCreatedAtTag::parse)
fun TagArray.postCount() = fastFirstNotNullOfOrNull(PostCountTag::parse)
fun TagArray.replyCount() = fastFirstNotNullOfOrNull(ReplyCountTag::parse)
fun TagArray.reactionsCount() = fastFirstNotNullOfOrNull(ReactionsCountTag::parse)
fun TagArray.zapAmountReceived() = fastFirstNotNullOfOrNull(ZapAmountReceivedTag::parse)
fun TagArray.zapAmountSent() = fastFirstNotNullOfOrNull(ZapAmountSentTag::parse)
fun TagArray.zapCountReceived() = fastFirstNotNullOfOrNull(ZapCountReceivedTag::parse)
fun TagArray.zapCountSent() = fastFirstNotNullOfOrNull(ZapCountSentTag::parse)
fun TagArray.zapAvgAmountDayReceived() = fastFirstNotNullOfOrNull(ZapAvgAmountDayReceivedTag::parse)
fun TagArray.zapAvgAmountDaySent() = fastFirstNotNullOfOrNull(ZapAvgAmountDaySentTag::parse)
fun TagArray.reportsCountReceived() = fastFirstNotNullOfOrNull(ReportsCountReceivedTag::parse)
fun TagArray.reportsCountSent() = fastFirstNotNullOfOrNull(ReportsCountSentTag::parse)
fun TagArray.topics() = mapNotNull(TopicTag::parse)
fun TagArray.activeHoursStart() = fastFirstNotNullOfOrNull(ActiveHoursStartTag::parse)
fun TagArray.activeHoursEnd() = fastFirstNotNullOfOrNull(ActiveHoursEndTag::parse)
fun TagArray.petName() = fastFirstNotNullOfOrNull(PetNameTag::parse)
@@ -23,7 +23,6 @@ package com.vitorpamplona.quartz.experimental.nip85TrustedAssertions
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.ContactCardEvent
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.PetNameTag
import com.vitorpamplona.quartz.nip85TrustedAssertions.users.tags.SummaryTag
@@ -77,11 +76,13 @@ class ContactCardPetNameTest {
)
val updated =
ContactCardEvent.updatePetNameAndSummary(
earlierVersion = card,
petName = "Bobby",
summary = "new summary",
signer = signer,
signer.sign(
ContactCardEvent.updatePetNameAndSummary(
earlierVersion = card,
petName = "Bobby",
summary = "new summary",
signer = signer,
),
)
assertEquals(targetUser, updated.aboutUser())
@@ -109,11 +110,13 @@ class ContactCardPetNameTest {
)
val cleared =
ContactCardEvent.updatePetNameAndSummary(
earlierVersion = card,
petName = null,
summary = null,
signer = signer,
signer.sign(
ContactCardEvent.updatePetNameAndSummary(
earlierVersion = card,
petName = null,
summary = null,
signer = signer,
),
)
assertNull(cleared.privatePetName())
@@ -130,17 +133,19 @@ class ContactCardPetNameTest {
ContactCardEvent.create(
targetUser = targetUser,
petName = "Bob :wave:",
emojis = listOf(oldEmoji),
signer = signer,
privateInitializer = { emojis(listOf(oldEmoji)) },
)
assertEquals(listOf(oldEmoji), card.privateTags(signer)!!.mapNotNull(EmojiUrlTag::parse))
val updated =
ContactCardEvent.updatePetNameAndSummary(
earlierVersion = card,
petName = "Bob :soapbox:",
emojis = listOf(newEmoji),
signer = signer,
signer.sign(
ContactCardEvent.updatePetNameAndSummary(
earlierVersion = card,
petName = "Bob :soapbox:",
emojis = listOf(newEmoji),
signer = signer,
),
)
assertEquals("Bob :soapbox:", updated.privatePetName())
@@ -165,10 +170,12 @@ class ContactCardPetNameTest {
assertEquals("public bob", card.petName())
val updated =
ContactCardEvent.updatePetNameAndSummary(
earlierVersion = card,
petName = "private bob",
signer = signer,
signer.sign(
ContactCardEvent.updatePetNameAndSummary(
earlierVersion = card,
petName = "private bob",
signer = signer,
),
)
assertNull(updated.petName())