mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
fix(quartz): correct tag readers found by the graph link review
Reviewing every kind's links for the graph vocabulary turned up readers that return the wrong target, or accept values of the wrong shape: - nip19: pubKeys() no longer returns the key of a pasted nsec - nip18: QTag address parsers accepted event ids and rejected addresses - nip28: a hide-message no longer counts its channel as a hidden message - nip53: live chat unmarkedReplyTos drops the activity; presence events mark their room `root`, as the spec requires - nip51: GeohashListEvent.create no longer swaps public and private entries; a replaceable list ignores a stray d tag in its address - forks: forkFromAddress reads the fork-marked `a`, not the first `a`; ForkTag checks the NIP-text kind, not the community kind - attestations: RequestTag parses as RequestTag and requires kind 31872; attestorPubKeys() names the p tags, assertionPubkey() is deprecated - nip58: profile badges read only kind 30009 definitions - interactive stories: rootScene writes the root-scene tag; reading-state build no longer swaps summary and image - nip87: mint and recommendation events are addressable (EventCache now routes them through consumeBaseReplaceable) - nipA0: voice replies carry the thread's root event, kind and author - id-shaped tags (replaced-by, agent, report, winner, edit, consent, group participants and deletions, citation timestamp, address members) reject values that are not 64-char hex or valid addresses Each fix has a regression test that fails without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pu8Fpp4KbXaiax8YTYxhJm
This commit is contained in:
+4
-10
@@ -3921,6 +3921,10 @@ open class EventCache :
|
||||
is PublicationContentEvent,
|
||||
is RelayReviewEvent,
|
||||
is EntityRatingEvent,
|
||||
// NIP-87 mint announcements and recommendations (kind 38172 / 38173 / 38000).
|
||||
is CashuMintEvent,
|
||||
is FedimintEvent,
|
||||
is MintRecommendationEvent,
|
||||
-> consumeBaseReplaceable(event, relay, wasVerified)
|
||||
|
||||
// ============================================================
|
||||
@@ -3937,16 +3941,6 @@ open class EventCache :
|
||||
is CashuTokenEvent,
|
||||
is CashuSpendingHistoryEvent,
|
||||
is CashuMintQuoteEvent,
|
||||
// NIP-87 Cashu mint discovery + recommendations: all three are kind 3xxxx
|
||||
// (parameterized-replaceable per the spec) but neither CashuMintEvent /
|
||||
// FedimintEvent / MintRecommendationEvent extends AddressableEvent in Quartz
|
||||
// today, so consumeBaseReplaceable's `check(event is AddressableEvent)` would
|
||||
// crash. Route them as regular events — downstream consumers
|
||||
// (CashuMintDirectoryState, CashuWalletState) already dedupe by (pubKey, dTag)
|
||||
// and keep the newest.
|
||||
is CashuMintEvent,
|
||||
is FedimintEvent,
|
||||
is MintRecommendationEvent,
|
||||
is ChatMessageEncryptedFileHeaderEvent,
|
||||
is ChatMessageEvent,
|
||||
is BirdDetectionEvent,
|
||||
|
||||
+1
-1
@@ -292,7 +292,7 @@ fun RenderAttestationRequest(
|
||||
|
||||
val aboutAddress = remember(noteEvent) { noteEvent.assertionAddress() }
|
||||
val aboutEvent = remember(noteEvent) { noteEvent.assertionEventId() }
|
||||
val aboutPubkey = remember(noteEvent) { noteEvent.assertionPubkey() }
|
||||
val aboutPubkey = remember(noteEvent) { noteEvent.attestorPubKeys().firstOrNull() }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.buzz.amTurnMetrics.tags
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
@@ -38,7 +39,7 @@ object AgentTag {
|
||||
fun parse(tag: Tag): HexKey? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(tag[1].length == 64 && Hex.isHex(tag[1])) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.buzz.iaIdentityArchival.tags
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
@@ -48,7 +49,8 @@ object ConsentTag {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
// The actor is a pubkey.
|
||||
ensure(tag[2].length == 64 && Hex.isHex(tag[2])) { return null }
|
||||
return Consent(tag[1], tag[2])
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.buzz.iaIdentityArchival.tags
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
@@ -40,7 +41,7 @@ object ReplacedByTag {
|
||||
fun parse(tag: Array<String>): HexKey? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(tag[1].length == 64 && Hex.isHex(tag[1])) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.buzz.moderation.tags
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
@@ -41,7 +42,7 @@ object ReportTag {
|
||||
fun parse(tag: Array<String>): HexKey? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(tag[1].length == 64 && Hex.isHex(tag[1])) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+12
-11
@@ -20,14 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.attestations.attestation.tags
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.approval.tags.ApprovedAddressTag
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
@@ -43,8 +42,10 @@ class RequestTag(
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "request"
|
||||
private val REQUEST_KIND_STR = AttestationRequestEvent.KIND.toString()
|
||||
|
||||
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && !Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)
|
||||
// The request an attestation answers is always a kind 31872 attestation request.
|
||||
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && Address.isOfKind(tag[1], REQUEST_KIND_STR)
|
||||
|
||||
fun isTagged(
|
||||
tag: Array<String>,
|
||||
@@ -53,7 +54,7 @@ class RequestTag(
|
||||
|
||||
fun isTagged(
|
||||
tag: Array<String>,
|
||||
address: ApprovedAddressTag,
|
||||
address: RequestTag,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address.toTag()
|
||||
|
||||
fun isIn(
|
||||
@@ -61,20 +62,20 @@ class RequestTag(
|
||||
addressIds: Set<String>,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] in addressIds
|
||||
|
||||
fun parse(tag: Array<String>): ApprovedAddressTag? {
|
||||
fun parse(tag: Array<String>): RequestTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||
ensure(Address.isOfKind(tag[1], REQUEST_KIND_STR)) { return null }
|
||||
|
||||
val address = Address.parse(tag[1]) ?: return null
|
||||
val relayHint = tag.getOrNull(2)?.let { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||
return ApprovedAddressTag(address, relayHint)
|
||||
return RequestTag(address, relayHint)
|
||||
}
|
||||
|
||||
fun parseValidAddress(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||
ensure(Address.isOfKind(tag[1], REQUEST_KIND_STR)) { return null }
|
||||
return Address.parse(tag[1])?.toValue()
|
||||
}
|
||||
|
||||
@@ -83,21 +84,21 @@ class RequestTag(
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
val address = Address.parse(tag[1]) ?: return null
|
||||
ensure(address.kind != CommunityDefinitionEvent.KIND) { return null }
|
||||
ensure(address.kind == AttestationRequestEvent.KIND) { return null }
|
||||
return address
|
||||
}
|
||||
|
||||
fun parseAddressId(tag: Array<String>): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||
ensure(Address.isOfKind(tag[1], REQUEST_KIND_STR)) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun parseAsHint(tag: Array<String>): AddressHint? {
|
||||
ensure(tag.has(2)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(!Address.isOfKind(tag[1], CommunityDefinitionEvent.KIND_STR)) { return null }
|
||||
ensure(Address.isOfKind(tag[1], REQUEST_KIND_STR)) { return null }
|
||||
ensure(tag[1].contains(':')) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
|
||||
|
||||
+5
@@ -86,8 +86,13 @@ class AttestationRequestEvent(
|
||||
|
||||
fun assertionETag() = tags.firstNotNullOfOrNull(ETag::parse)
|
||||
|
||||
/** The attestors this request asks (its `p` tags, written by [buildEvent]'s `attestorPubKeys`). */
|
||||
fun attestorPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
@Deprecated("Returns the first ATTESTOR, not the assertion's author", ReplaceWith("attestorPubKeys().firstOrNull()"))
|
||||
fun assertionPubkey() = tags.firstNotNullOfOrNull(PTag::parseKey)
|
||||
|
||||
@Deprecated("Returns the first ATTESTOR's tag, not the assertion's author")
|
||||
fun assertionPTag() = tags.firstNotNullOfOrNull(PTag::parse)
|
||||
|
||||
companion object {
|
||||
|
||||
+2
-1
@@ -28,6 +28,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip23LongContent.tags.TitleTag
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/** A citation of something on the web (kind 31): a URL, optionally timestamped. */
|
||||
@@ -50,7 +51,7 @@ class ExternalCitationEvent(
|
||||
fun url() = value(CitationTags.URL) ?: value("url")
|
||||
|
||||
/** The id of a NIP-03 kind-1040 timestamp attesting when the page was seen. */
|
||||
fun openTimestamp() = value(CitationTags.OPEN_TIMESTAMP)
|
||||
fun openTimestamp() = value(CitationTags.OPEN_TIMESTAMP)?.takeIf { it.length == 64 && Hex.isHex(it) }
|
||||
|
||||
override fun displayTitle(): String? = title() ?: url()
|
||||
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.experimental.forks
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip10Notes.tags.MarkedETag
|
||||
|
||||
/**
|
||||
* The address an `a` tag marked `fork` points at: `["a", <address>, <relay>, "fork"]`, the
|
||||
* version a note, a NIP text or a wiki article (NIP-54 "Forks") was forked from — of any kind.
|
||||
* Only the marked tag counts: an event also carries unmarked `a` tags (a community, a mention),
|
||||
* and those are not its origin.
|
||||
*/
|
||||
fun parseForkedAddress(tag: Array<String>): Address? {
|
||||
if (tag.size < 4 || tag[0] != "a") return null
|
||||
if (tag[3] != MarkedETag.MARKER.FORK.code) return null
|
||||
return Address.parse(tag[1])
|
||||
}
|
||||
+2
-2
@@ -128,8 +128,8 @@ class InteractiveStoryReadingStateEvent(
|
||||
status(status)
|
||||
|
||||
root.event.title()?.let { storyTitle(it) }
|
||||
root.event.summary()?.let { storyImage(it) }
|
||||
root.event.image()?.let { storySummary(it) }
|
||||
root.event.summary()?.let { storySummary(it) }
|
||||
root.event.image()?.let { storyImage(it) }
|
||||
|
||||
initializer()
|
||||
}
|
||||
|
||||
+4
-1
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.quartz.experimental.interactiveStories
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.tags.ReadStatusTag
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.tags.RootSceneTag
|
||||
import com.vitorpamplona.quartz.experimental.interactiveStories.tags.StoryOptionTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
@@ -51,7 +52,9 @@ fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.storyImage(imageUrl: Stri
|
||||
|
||||
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.storyImages(imageUrls: List<String>) = addAll(imageUrls.map { ImageTag.assemble(it) })
|
||||
|
||||
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.rootScene(scene: ATag) = addUnique(scene.toATagArray())
|
||||
// The root is the uppercase `A` (RootSceneTag): written as a lowercase `a`, the current scene
|
||||
// (also an `a`) replaced it and root() found nothing.
|
||||
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.rootScene(scene: ATag) = addUnique(RootSceneTag.assemble(scene.toTag(), scene.relay))
|
||||
|
||||
fun TagArrayBuilder<InteractiveStoryReadingStateEvent>.currentScene(scene: ATag) = addUnique(scene.toATagArray())
|
||||
|
||||
|
||||
+2
-2
@@ -22,8 +22,8 @@ package com.vitorpamplona.quartz.experimental.nipsOnNostr
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
|
||||
import com.vitorpamplona.quartz.experimental.forks.parseForkedAddress
|
||||
import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -113,7 +113,7 @@ class NipTextEvent(
|
||||
|
||||
override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" }
|
||||
|
||||
override fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress)
|
||||
override fun forkFromAddress() = tags.firstNotNullOfOrNull(::parseForkedAddress)
|
||||
|
||||
override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId)
|
||||
|
||||
|
||||
+2
-3
@@ -26,7 +26,6 @@ import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.AddressHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
@@ -66,7 +65,7 @@ class ForkTag(
|
||||
ensure(
|
||||
Address.Companion.isOfKind(
|
||||
tag[1],
|
||||
CommunityDefinitionEvent.Companion.KIND_STR,
|
||||
NipTextEvent.KIND_STR,
|
||||
),
|
||||
) { return null }
|
||||
|
||||
@@ -81,7 +80,7 @@ class ForkTag(
|
||||
ensure(
|
||||
Address.Companion.isOfKind(
|
||||
tag[1],
|
||||
CommunityDefinitionEvent.Companion.KIND_STR,
|
||||
NipTextEvent.KIND_STR,
|
||||
),
|
||||
) { return null }
|
||||
return Address.Companion.parse(tag[1])?.toValue()
|
||||
|
||||
+2
@@ -69,6 +69,7 @@ data class AddressMemberTag(
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(AddressSerializer.parse(tag[1]) != null) { return null }
|
||||
|
||||
return AddressMemberTag(tag[1], MemberTagFields.relayHint(tag), MemberTagFields.score(tag))
|
||||
}
|
||||
@@ -77,6 +78,7 @@ data class AddressMemberTag(
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(AddressSerializer.parse(tag[1]) != null) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip10Notes
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
|
||||
import com.vitorpamplona.quartz.experimental.forks.parseForkedAddress
|
||||
import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
@@ -124,7 +125,7 @@ class TextNoteEvent(
|
||||
|
||||
override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" }
|
||||
|
||||
override fun forkFromAddress() = tags.firstNotNullOfOrNull(ATag::parseAddress)
|
||||
override fun forkFromAddress() = tags.firstNotNullOfOrNull(::parseForkedAddress)
|
||||
|
||||
override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId)
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ interface QTag {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length != 64) { return null }
|
||||
ensure(!tag[1].contains(':')) { return null }
|
||||
ensure(tag[1].contains(':')) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ interface QTag {
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length != 64) { return null }
|
||||
ensure(tag[2].isNotEmpty()) { return null }
|
||||
ensure(!tag[1].contains(':')) { return null }
|
||||
ensure(tag[1].contains(':')) { return null }
|
||||
|
||||
val relayHint = pickRelayHint(tag)
|
||||
ensure(relayHint != null) { return null }
|
||||
|
||||
@@ -32,7 +32,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NNote
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NProfile
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
|
||||
|
||||
fun NEvent.toEventHint() = relay.map { EventIdHint(hex, it) }
|
||||
|
||||
@@ -86,12 +85,16 @@ fun List<Entity>.pubKeyHints(): List<PubKeyHint> =
|
||||
}
|
||||
}.flatten()
|
||||
|
||||
/**
|
||||
* The pubkeys cited as `npub` / `nprofile`. An `nsec` is deliberately NOT here: its hex is a
|
||||
* PRIVATE key, so reporting a pasted one as a "linked pubkey" would publish the secret to every
|
||||
* index, hint store and relay filter that consumes this list.
|
||||
*/
|
||||
fun List<Entity>.pubKeys(): List<HexKey> =
|
||||
mapNotNull { entity ->
|
||||
when (entity) {
|
||||
is NProfile -> entity.hex
|
||||
is NPub -> entity.hex
|
||||
is NSec -> entity.hex
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
+14
-1
@@ -47,7 +47,20 @@ class ChannelHideMessageEvent(
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(ETag::parseId)
|
||||
|
||||
fun eventsToHide() = tags.taggedEventIds()
|
||||
/**
|
||||
* NIP-28 names the channel only through a MARKED root (Quartz writes one; the spec's own 43
|
||||
* has none): the unmarked-root fallback of [BasePublicChatEvent] would read the first hidden
|
||||
* message as the channel.
|
||||
*/
|
||||
override fun channel() = markedRoot()
|
||||
|
||||
override fun channelId() = channel()?.eventId
|
||||
|
||||
/** The hidden messages: every `e` except the channel it is posted in. */
|
||||
fun eventsToHide(): List<HexKey> {
|
||||
val channel = channelId()
|
||||
return tags.taggedEventIds().filter { it != channel }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val KIND = 43
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.mapValueTagged
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/**
|
||||
@@ -48,7 +49,7 @@ class GroupParticipantsEvent(
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun groupId() = dTag()
|
||||
|
||||
fun participants(): List<HexKey> = tags.mapValueTagged(TAG_NAME) { it }
|
||||
fun participants(): List<HexKey> = tags.mapValueTagged(TAG_NAME) { it }.filter { it.length == 64 && Hex.isHex(it) }
|
||||
|
||||
companion object {
|
||||
const val KIND = 39004
|
||||
|
||||
+2
-1
@@ -35,6 +35,7 @@ import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupIdTag
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.tags.GroupPin
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.tags.ParentTag
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.tags.PreviousTag
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
|
||||
fun TagArray.groupId() = firstTagValue(GroupIdTag.TAG_NAME)
|
||||
|
||||
@@ -56,7 +57,7 @@ fun TagArray.childGroupIds(): List<String> = mapNotNull(ChildTag::parse)
|
||||
|
||||
fun TagArray.userPubKeys(): List<HexKey> = mapNotNull(PTag::parseKey)
|
||||
|
||||
fun TagArray.deletedEventIds(): List<HexKey> = mapValueTagged("e") { it }
|
||||
fun TagArray.deletedEventIds(): List<HexKey> = mapValueTagged("e") { it }.filter { it.length == 64 && Hex.isHex(it) }
|
||||
|
||||
/** The ordered pin list: `e` (event id) and `a` (address) references, interleaved as sent. */
|
||||
fun TagArray.groupPins(): List<GroupPin> = mapNotNull(GroupPin::parse)
|
||||
|
||||
+8
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.isReplaceable
|
||||
import com.vitorpamplona.quartz.nip01Core.diff.ContentChange
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
|
||||
@@ -42,6 +43,13 @@ abstract class PrivateTagArrayEvent(
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
override fun isContentEncoded() = true
|
||||
|
||||
/**
|
||||
* A NIP-51 LIST (10000–19999) is replaceable: NIP-01 fixes its address to `kind:pubkey:`
|
||||
* whatever tags it carries. Read from the tags, a stray `d` would split one user's list
|
||||
* into several addresses. SETS (30000–39999) are addressed by their `d`.
|
||||
*/
|
||||
override fun dTag(): String = if (kind.isReplaceable()) "" else super.dTag()
|
||||
|
||||
/**
|
||||
* How the NIP-44 encrypted private items changed since [older]. They can't be compared
|
||||
* item by item without decrypting, so only as a whole.
|
||||
|
||||
+2
-2
@@ -198,8 +198,8 @@ class GeohashListEvent(
|
||||
signer: NostrSignerSync,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
): GeohashListEvent {
|
||||
val privateTagArray = publicGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray()
|
||||
val publicTagArray = privateGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray()
|
||||
val publicTagArray = publicGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray()
|
||||
val privateTagArray = privateGeohashes.map { GeoHashTag.assembleSingle(it) }.toTypedArray()
|
||||
return signer.signNip51List(createdAt, KIND, publicTagArray, privateTagArray)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ class LiveActivitiesChatMessageEvent(
|
||||
|
||||
override fun markedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "")
|
||||
|
||||
override fun unmarkedReplyTos() = super.markedReplyTos().minus(activityHex() ?: "")
|
||||
override fun unmarkedReplyTos() = super.unmarkedReplyTos().minus(activityHex() ?: "")
|
||||
|
||||
override fun exposeInDraft() =
|
||||
tagArray<LiveActivitiesChatMessageEvent> {
|
||||
|
||||
+8
@@ -37,10 +37,18 @@ class MeetingSpaceTag(
|
||||
|
||||
fun toTagArray() = assemble(address, relayHint)
|
||||
|
||||
/**
|
||||
* The form a kind 10312 presence uses: NIP-53 marks the room it points at as its `root`
|
||||
* (`["a", <room>, <relay>, "root"]`). A meeting room (30313) references its space WITHOUT a
|
||||
* marker, so this is not [toTagArray].
|
||||
*/
|
||||
fun toRootTagArray() = arrayOf(TAG_NAME, address.toValue(), relayHint?.url ?: "", ROOT_MARKER)
|
||||
|
||||
fun toTagIdOnly() = assemble(address, null)
|
||||
|
||||
companion object Companion {
|
||||
const val TAG_NAME = "a"
|
||||
const val ROOT_MARKER = "root"
|
||||
|
||||
fun isTagged(tag: Array<String>) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
|
||||
+3
-3
@@ -22,7 +22,6 @@ package com.vitorpamplona.quartz.nip53LiveActivities.presence
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.toATag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.MeetingRoomEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.MeetingSpaceTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.HandRaisedTag
|
||||
@@ -30,9 +29,10 @@ import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.MutedTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.OnstageTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.presence.tags.PublishingTag
|
||||
|
||||
fun TagArrayBuilder<MeetingRoomPresenceEvent>.roomMeeting(rep: MeetingSpaceTag) = addUnique(rep.toTagArray())
|
||||
// NIP-53: a presence points at its room with the `root` marker.
|
||||
fun TagArrayBuilder<MeetingRoomPresenceEvent>.roomMeeting(rep: MeetingSpaceTag) = addUnique(rep.toRootTagArray())
|
||||
|
||||
fun TagArrayBuilder<MeetingRoomPresenceEvent>.roomMeeting(rep: EventHintBundle<MeetingRoomEvent>) = addUnique(rep.toATag().toATagArray())
|
||||
fun TagArrayBuilder<MeetingRoomPresenceEvent>.roomMeeting(rep: EventHintBundle<MeetingRoomEvent>) = addUnique(MeetingSpaceTag(rep.event.address(), rep.relay).toRootTagArray())
|
||||
|
||||
fun TagArrayBuilder<MeetingRoomPresenceEvent>.handRaised(raised: Boolean) = addUnique(HandRaisedTag.assemble(raised))
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ package com.vitorpamplona.quartz.nip54Wiki
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.forks.IForkableEvent
|
||||
import com.vitorpamplona.quartz.experimental.forks.parseForkedAddress
|
||||
import com.vitorpamplona.quartz.experimental.forks.parseForkedEventId
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
@@ -150,7 +150,7 @@ class WikiArticleEvent(
|
||||
|
||||
override fun isAFork() = tags.any { it.size > 3 && (it[0] == "a" || it[0] == "e") && it[3] == "fork" }
|
||||
|
||||
override fun forkFromAddress() = tags.firstNotNullOfOrNull(ForkTag::parseAddress)
|
||||
override fun forkFromAddress() = tags.firstNotNullOfOrNull(::parseForkedAddress)
|
||||
|
||||
override fun forkFromVersion() = tags.firstNotNullOfOrNull(MarkedETag::parseForkedEventId)
|
||||
|
||||
|
||||
+6
-1
@@ -24,9 +24,14 @@ import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.taggedAddresses
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEvents
|
||||
import com.vitorpamplona.quartz.nip58Badges.accepted.tags.AcceptedBadge
|
||||
import com.vitorpamplona.quartz.nip58Badges.definition.BadgeDefinitionEvent
|
||||
|
||||
fun TagArray.acceptedBadges() = AcceptedBadge.parseAll(this)
|
||||
|
||||
fun TagArray.badgeAwardEvents() = taggedEvents()
|
||||
|
||||
fun TagArray.badgeAwardDefinitions() = taggedAddresses()
|
||||
/**
|
||||
* The badge DEFINITIONS (kind 30009) a profile displays. NIP-58 profiles also carry `a` tags
|
||||
* pointing at badge SETS (kind 30008), which are not definitions.
|
||||
*/
|
||||
fun TagArray.badgeAwardDefinitions() = taggedAddresses().filter { it.kind == BadgeDefinitionEvent.KIND }
|
||||
|
||||
+3
-1
@@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nip64Chess.end.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
class WinnerTag {
|
||||
@@ -32,7 +33,8 @@ class WinnerTag {
|
||||
|
||||
fun parse(tag: Array<String>): HexKey? {
|
||||
ensure(tag.has(1) && tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
// The winner is a pubkey.
|
||||
ensure(tag[1].length == 64 && Hex.isHex(tag[1])) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.quartz.nip87Ecash.cashu
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
@@ -36,15 +36,13 @@ class CashuMintEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun mintUrl() = tags.mintUrl()
|
||||
|
||||
fun nuts() = tags.nuts()
|
||||
|
||||
fun network() = tags.network()
|
||||
|
||||
fun dTag() = tags.dTag()
|
||||
|
||||
companion object {
|
||||
const val KIND = 38172
|
||||
|
||||
|
||||
+2
-4
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.quartz.nip87Ecash.fedimint
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
@@ -36,15 +36,13 @@ class FedimintEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
|
||||
fun inviteCodes() = tags.inviteCodes()
|
||||
|
||||
fun modules() = tags.modules()
|
||||
|
||||
fun network() = tags.network()
|
||||
|
||||
fun dTag() = tags.dTag()
|
||||
|
||||
companion object {
|
||||
const val KIND = 38173
|
||||
|
||||
|
||||
+2
-4
@@ -21,7 +21,7 @@
|
||||
package com.vitorpamplona.quartz.nip87Ecash.recommendation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
@@ -39,7 +39,7 @@ class MintRecommendationEvent(
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : Event(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
SearchableEvent {
|
||||
override fun indexableContent() = content
|
||||
|
||||
@@ -53,8 +53,6 @@ class MintRecommendationEvent(
|
||||
|
||||
fun mintEventKind() = tags.mintEventKind()
|
||||
|
||||
fun dTag() = tags.dTag()
|
||||
|
||||
fun mintEventAddresses() = tags.mintEventAddresses()
|
||||
|
||||
fun isCashuRecommendation() = mintEventKind() == CashuMintEvent.KIND
|
||||
|
||||
+19
@@ -23,6 +23,9 @@ package com.vitorpamplona.quartz.nipA0VoiceMessages
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip22Comments.tags.RootAuthorTag
|
||||
import com.vitorpamplona.quartz.nip22Comments.tags.RootEventTag
|
||||
import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.tags.HashSha256Tag
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyAuthorTag
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyEventTag
|
||||
@@ -47,6 +50,22 @@ fun <T : BaseVoiceEvent> TagArrayBuilder<T>.audioIMeta(audioUrls: List<AudioMeta
|
||||
return this
|
||||
}
|
||||
|
||||
// NIP-22 root scope (uppercase E / K / P): NIP-A0 says a voice reply MUST follow NIP-22.
|
||||
fun TagArrayBuilder<VoiceReplyEvent>.rootEvent(
|
||||
eventId: String,
|
||||
relayHint: NormalizedRelayUrl?,
|
||||
pubkey: String?,
|
||||
) = addUnique(RootEventTag.assemble(eventId, relayHint, pubkey))
|
||||
|
||||
fun TagArrayBuilder<VoiceReplyEvent>.rootKind(kind: String) = addUnique(RootKindTag.assemble(kind))
|
||||
|
||||
fun TagArrayBuilder<VoiceReplyEvent>.rootKind(kind: Int) = addUnique(RootKindTag.assemble(kind))
|
||||
|
||||
fun TagArrayBuilder<VoiceReplyEvent>.rootAuthor(
|
||||
pubKey: HexKey,
|
||||
relay: NormalizedRelayUrl?,
|
||||
) = addUnique(RootAuthorTag.assemble(pubKey, relay))
|
||||
|
||||
fun TagArrayBuilder<VoiceReplyEvent>.replyEvent(
|
||||
eventId: String,
|
||||
relayHint: NormalizedRelayUrl?,
|
||||
|
||||
+26
@@ -24,6 +24,9 @@ import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip22Comments.tags.RootAuthorTag
|
||||
import com.vitorpamplona.quartz.nip22Comments.tags.RootEventTag
|
||||
import com.vitorpamplona.quartz.nip22Comments.tags.RootKindTag
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyAuthorTag
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyEventTag
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.tags.ReplyKindTag
|
||||
@@ -55,6 +58,12 @@ class VoiceReplyEvent(
|
||||
|
||||
fun replyingTo(): HexKey? = tags.lastNotNullOfOrNull(ReplyEventTag::parseKey)
|
||||
|
||||
/** The thread's root scope (NIP-22 `E`): the voice message the conversation started from. */
|
||||
fun rootEventId(): HexKey? = tags.firstNotNullOfOrNull(RootEventTag::parseKey)
|
||||
|
||||
/** The root scope's author (NIP-22 `P`). */
|
||||
fun rootAuthorKey(): HexKey? = tags.firstNotNullOfOrNull(RootAuthorTag::parseKey)
|
||||
|
||||
companion object {
|
||||
const val KIND = 1244
|
||||
|
||||
@@ -73,6 +82,23 @@ class VoiceReplyEvent(
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<VoiceReplyEvent>.() -> Unit = {},
|
||||
) = build(voiceMessage, KIND, createdAt) {
|
||||
// NIP-A0: a voice reply MUST follow NIP-22, so it names the thread's root scope
|
||||
// (E / K / P) as well as its parent. Replying to a reply inherits that reply's root;
|
||||
// replying to the voice message itself makes it the root.
|
||||
val parent = replyingTo.event
|
||||
val inherited =
|
||||
if (parent is VoiceReplyEvent) {
|
||||
parent.tags.filter { RootEventTag.match(it) || RootKindTag.match(it) || RootAuthorTag.match(it) }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
if (inherited.any { RootEventTag.match(it) }) {
|
||||
inherited.forEach { addUnique(it) }
|
||||
} else {
|
||||
rootEvent(parent.id, replyingTo.relay, parent.pubKey)
|
||||
rootKind(parent.kind)
|
||||
rootAuthor(parent.pubKey, replyingTo.authorHomeRelay)
|
||||
}
|
||||
replyEvent(replyingTo.event.id, replyingTo.relay, replyingTo.event.pubKey)
|
||||
replyKind(replyingTo.event.kind)
|
||||
replyAuthor(replyingTo.event.pubKey, replyingTo.authorHomeRelay)
|
||||
|
||||
+2
-1
@@ -22,6 +22,7 @@ package com.vitorpamplona.quartz.nipXXPodcasting20.episode.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.Hex
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
@@ -36,7 +37,7 @@ class EditTag {
|
||||
fun parse(tag: Array<String>): HexKey? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
ensure(tag[1].length == 64 && Hex.isHex(tag[1])) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.experimental.attestations
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.attestations.attestation.tags.RequestTag
|
||||
import com.vitorpamplona.quartz.experimental.attestations.request.AttestationRequestEvent
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class RequestTagTest {
|
||||
private val pk = "1".repeat(64)
|
||||
|
||||
@Test
|
||||
fun aRequestTagPointsAtAnAttestationRequest() {
|
||||
val request = "${AttestationRequestEvent.KIND}:$pk:claim"
|
||||
val parsed: RequestTag? = RequestTag.parse(arrayOf("request", request))
|
||||
assertEquals(request, parsed?.toTag())
|
||||
assertEquals(request, RequestTag.parseAddressId(arrayOf("request", request)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anythingElseIsNotARequest() {
|
||||
assertNull(RequestTag.parse(arrayOf("request", "30023:$pk:post")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theRequestsPTagsAreItsAttestors() {
|
||||
val a = "a".repeat(64)
|
||||
val b = "b".repeat(64)
|
||||
val event = AttestationRequestEvent("0".repeat(64), pk, 1, arrayOf(arrayOf("d", "x"), arrayOf("p", a), arrayOf("p", b)), "", "0".repeat(128))
|
||||
assertEquals(listOf(a, b), event.attestorPubKeys())
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.experimental.forks
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.tags.ForkTag
|
||||
import com.vitorpamplona.quartz.nip10Notes.TextNoteEvent
|
||||
import com.vitorpamplona.quartz.nip54Wiki.WikiArticleEvent
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
|
||||
class ForkedAddressTest {
|
||||
private val pk = "1".repeat(64)
|
||||
private val community = "34550:$pk:group"
|
||||
|
||||
@Test
|
||||
fun aNoteForksFromItsMarkedAddressNotItsCommunity() {
|
||||
val origin = "30023:$pk:post"
|
||||
val note = TextNoteEvent("0".repeat(64), pk, 1, arrayOf(arrayOf("a", community), arrayOf("a", origin, "", "fork")), "", "0".repeat(128))
|
||||
assertEquals(origin, note.forkFromAddress()?.toValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aWikiArticleFindsTheArticleItWasForkedFrom() {
|
||||
val origin = "30818:$pk:bitcoin"
|
||||
val article = WikiArticleEvent("0".repeat(64), pk, 1, arrayOf(arrayOf("d", "bitcoin"), arrayOf("a", origin, "", "fork")), "", "0".repeat(128))
|
||||
assertEquals(origin, article.forkFromAddress()?.toValue())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aNipTextForkTagIsItsOwnKind() {
|
||||
val origin = "${NipTextEvent.KIND}:$pk:nip-01"
|
||||
assertNotNull(ForkTag.parse(arrayOf("a", origin, "", "fork")))
|
||||
assertEquals(origin, ForkTag.parseValidAddress(arrayOf("a", origin, "", "fork")))
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.experimental.interactiveStories
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ReadingStateBuildTest {
|
||||
@Test
|
||||
fun theReadingStateKeepsItsRootAndItsSummaryAndImage() {
|
||||
val pk = "1".repeat(64)
|
||||
val prologue =
|
||||
InteractiveStoryPrologueEvent(
|
||||
"0".repeat(64),
|
||||
pk,
|
||||
1,
|
||||
arrayOf(arrayOf("d", "story"), arrayOf("title", "A Story"), arrayOf("summary", "the summary"), arrayOf("image", "https://img.example/cover.png")),
|
||||
"",
|
||||
"0".repeat(128),
|
||||
)
|
||||
val scene = InteractiveStorySceneEvent("2".repeat(64), pk, 2, arrayOf(arrayOf("d", "scene-2")), "", "0".repeat(128))
|
||||
val state = NostrSignerSync().sign(InteractiveStoryReadingStateEvent.build(EventHintBundle(prologue), EventHintBundle(scene)))
|
||||
|
||||
assertEquals(prologue.address().toValue(), state.root()?.toTag())
|
||||
assertEquals(scene.address().toValue(), state.currentScene()?.toValue())
|
||||
assertEquals("the summary", state.summary())
|
||||
assertEquals("https://img.example/cover.png", state.image())
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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.nip18Reposts.quotes
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class QTagAddressTest {
|
||||
private val address = "30023:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:my-article"
|
||||
|
||||
@Test
|
||||
fun anAddressQuoteParsesAsAnAddress() {
|
||||
assertEquals(address, QTag.parseAddressId(arrayOf("q", address)))
|
||||
assertEquals(address, QTag.parseAddressAsHint(arrayOf("q", address, "wss://relay.example.com"))?.addressId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anEventQuoteIsNotAnAddress() {
|
||||
assertNull(QTag.parseAddressId(arrayOf("q", "a".repeat(64))))
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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.nip19Bech32
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class PubKeysNeverLeakNsecTest {
|
||||
private val secret = "7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a"
|
||||
private val npubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
|
||||
|
||||
@Test
|
||||
fun aPastedNsecIsNotALinkedPubkey() {
|
||||
val nsec = secret.hexToByteArray().toNsec()
|
||||
val npub = npubKey.hexToByteArray().toNpub()
|
||||
val cited = Nip19Parser.parseAll("leaked nostr:$nsec and hi nostr:$npub")
|
||||
assertEquals(listOf(npubKey), cited.pubKeys())
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.nip28PublicChat
|
||||
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelHideMessageEvent
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class ChannelHideMessageEventTest {
|
||||
private val channel = "c".repeat(64)
|
||||
private val message = "d".repeat(64)
|
||||
|
||||
private fun hide(vararg tags: Array<String>) = ChannelHideMessageEvent("0".repeat(64), "1".repeat(64), 1, arrayOf(*tags), "spam", "0".repeat(128))
|
||||
|
||||
@Test
|
||||
fun theChannelIsNotAmongTheHiddenMessages() {
|
||||
val event = hide(arrayOf("e", channel, "", "root"), arrayOf("e", message))
|
||||
assertEquals(channel, event.channelId())
|
||||
assertEquals(listOf(message), event.eventsToHide())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aSpecShapedHideDoesNotReadTheHiddenMessageAsTheChannel() {
|
||||
// NIP-28's own kind 43 carries only the message to hide.
|
||||
val event = hide(arrayOf("e", message))
|
||||
assertNull(event.channelId())
|
||||
assertEquals(listOf(message), event.eventsToHide())
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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.nip51Lists
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import com.vitorpamplona.quartz.nip51Lists.followSet.FollowSetEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.geohashList.GeohashListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.mediaFollowList.MediaFollowListEvent
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ListAddressAndGeohashTest {
|
||||
private val pk = "1".repeat(64)
|
||||
|
||||
@Test
|
||||
fun aStrayDTagDoesNotSplitAReplaceableListsAddress() {
|
||||
val list = MediaFollowListEvent("0".repeat(64), pk, 1, arrayOf(arrayOf("d", "stray")), "", "0".repeat(128))
|
||||
assertEquals("", list.dTag())
|
||||
assertEquals("${MediaFollowListEvent.KIND}:$pk:", list.addressTag())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aSetIsStillAddressedByItsD() {
|
||||
val set = FollowSetEvent("0".repeat(64), pk, 1, arrayOf(arrayOf("d", "friends")), "", "0".repeat(128))
|
||||
assertEquals("friends", set.dTag())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun privateGeohashesAreNeverPublishedInClear() {
|
||||
val event = GeohashListEvent.create(listOf("u4pr"), listOf("9q8y"), NostrSignerSync())
|
||||
val clear = event.tags.filter { it.size > 1 && it[0] == "g" }.map { it[1] }
|
||||
assertTrue("u4pr" in clear, "$clear")
|
||||
assertFalse("9q8y" in clear, "a private geohash leaked into the public tags: $clear")
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.nip53LiveActivities
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.tagArray
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.chat.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.meetingSpaces.tags.MeetingSpaceTag
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.presence.MeetingRoomPresenceEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.presence.roomMeeting
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class LiveActivitiesLinkFixesTest {
|
||||
private val pk = "1".repeat(64)
|
||||
|
||||
@Test
|
||||
fun unmarkedReplyTosAreTheUnmarkedOnes() {
|
||||
val marked = "a".repeat(64)
|
||||
val unmarked = "b".repeat(64)
|
||||
val chat =
|
||||
LiveActivitiesChatMessageEvent(
|
||||
"0".repeat(64),
|
||||
pk,
|
||||
1,
|
||||
arrayOf(arrayOf("a", "30311:$pk:stream", "", "root"), arrayOf("e", marked, "", "reply"), arrayOf("e", unmarked)),
|
||||
"hi",
|
||||
"0".repeat(128),
|
||||
)
|
||||
assertEquals(listOf(marked), chat.markedReplyTos())
|
||||
assertEquals(listOf(unmarked), chat.unmarkedReplyTos())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aPresencePointsAtItsRoomWithTheRootMarker() {
|
||||
val room = Address(30313, pk, "room")
|
||||
val tags = tagArray<MeetingRoomPresenceEvent> { roomMeeting(MeetingSpaceTag(room, null)) }
|
||||
assertEquals(listOf("a", room.toValue(), "", "root"), tags.single().toList())
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.nip58Badges
|
||||
|
||||
import com.vitorpamplona.quartz.nip58Badges.profile.ProfileBadgesEvent
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ProfileBadgeDefinitionsTest {
|
||||
@Test
|
||||
fun badgeSetPointersAreNotBadgeDefinitions() {
|
||||
val pk = "1".repeat(64)
|
||||
val definition = "30009:$pk:early-adopter"
|
||||
val profile =
|
||||
ProfileBadgesEvent(
|
||||
"0".repeat(64),
|
||||
pk,
|
||||
1,
|
||||
arrayOf(arrayOf("d", "profile_badges"), arrayOf("a", definition), arrayOf("e", "e".repeat(64)), arrayOf("a", "30008:$pk:favorites")),
|
||||
"",
|
||||
"0".repeat(128),
|
||||
)
|
||||
assertEquals(listOf(definition), profile.badgeAwardDefinitions().map { it.toValue() })
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.nip87Ecash
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressableEvent
|
||||
import com.vitorpamplona.quartz.nip87Ecash.cashu.CashuMintEvent
|
||||
import com.vitorpamplona.quartz.nip87Ecash.fedimint.FedimintEvent
|
||||
import com.vitorpamplona.quartz.nip87Ecash.recommendation.MintRecommendationEvent
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
|
||||
class MintEventsAreAddressableTest {
|
||||
private val pk = "1".repeat(64)
|
||||
private val tags = arrayOf(arrayOf("d", "mint-id"))
|
||||
|
||||
@Test
|
||||
fun theNip87KindsAreAddressable() {
|
||||
for (event in listOf(
|
||||
CashuMintEvent("0".repeat(64), pk, 1, tags, "", "0".repeat(128)),
|
||||
FedimintEvent("0".repeat(64), pk, 1, tags, "", "0".repeat(128)),
|
||||
MintRecommendationEvent("0".repeat(64), pk, 1, tags, "", "0".repeat(128)),
|
||||
)) {
|
||||
assertIs<AddressableEvent>(event)
|
||||
assertEquals("${event.kind}:$pk:mint-id", event.addressTag())
|
||||
}
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.nipA0VoiceMessages
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerSync
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class VoiceReplyRootScopeTest {
|
||||
private val audio = AudioMeta("https://blossom.example/a.m4a", "audio/mp4", "f".repeat(64), 3, listOf(0.1f))
|
||||
|
||||
@Test
|
||||
fun aVoiceReplyNamesItsThreadsRootScope() {
|
||||
val signer = NostrSignerSync()
|
||||
val voice = VoiceEvent("9".repeat(64), "1".repeat(64), 1, arrayOf(arrayOf("imeta", "url https://blossom.example/v.m4a")), "", "0".repeat(128))
|
||||
|
||||
val reply = signer.sign(VoiceReplyEvent.build(audio, EventHintBundle(voice)))
|
||||
assertEquals(voice.id, reply.rootEventId())
|
||||
assertEquals(voice.pubKey, reply.rootAuthorKey())
|
||||
assertEquals(voice.id, reply.replyingTo())
|
||||
|
||||
// A reply to that reply keeps the thread's root and names its new parent.
|
||||
val nested = signer.sign(VoiceReplyEvent.build(audio, EventHintBundle<BaseVoiceEvent>(reply)))
|
||||
assertEquals(voice.id, nested.rootEventId())
|
||||
assertEquals(reply.id, nested.replyingTo())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 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.utils
|
||||
|
||||
import com.vitorpamplona.quartz.buzz.amTurnMetrics.tags.AgentTag
|
||||
import com.vitorpamplona.quartz.buzz.iaIdentityArchival.tags.ConsentTag
|
||||
import com.vitorpamplona.quartz.buzz.iaIdentityArchival.tags.ReplacedByTag
|
||||
import com.vitorpamplona.quartz.buzz.moderation.tags.ReportTag
|
||||
import com.vitorpamplona.quartz.nip64Chess.end.tags.WinnerTag
|
||||
import com.vitorpamplona.quartz.nipXXPodcasting20.episode.tags.EditTag
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
/** Tags whose value IS a pubkey or event id must not hand back anything else as one. */
|
||||
class IdShapedTagParsersTest {
|
||||
private val key = "a".repeat(64)
|
||||
|
||||
@Test
|
||||
fun onlyHexIdsParse() {
|
||||
for ((name, parse) in listOf<Pair<String, (Array<String>) -> String?>>(
|
||||
ReplacedByTag.TAG_NAME to ReplacedByTag::parse,
|
||||
AgentTag.TAG_NAME to AgentTag::parse,
|
||||
ReportTag.TAG_NAME to ReportTag::parse,
|
||||
WinnerTag.TAG_NAME to WinnerTag::parse,
|
||||
EditTag.TAG_NAME to EditTag::parse,
|
||||
)) {
|
||||
assertEquals(key, parse(arrayOf(name, key)), name)
|
||||
assertNull(parse(arrayOf(name, "not-a-key")), name)
|
||||
assertNull(parse(arrayOf(name, "g".repeat(64))), name)
|
||||
}
|
||||
assertNull(ConsentTag.parse(arrayOf(ConsentTag.TAG_NAME, "path", "not-a-key")))
|
||||
assertEquals(key, ConsentTag.parse(arrayOf(ConsentTag.TAG_NAME, "path", key))?.actorPubKey)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user