mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
feat(quartz): implement the Tapestry Trusted List family (30392-30395)
Adds the pre-NIP Trusted List kinds as a companion to NIP-85 Trusted Assertions. A Trusted List is an addressable event that publishes a curated set of members computed under a point of view -- the aggregate analog of an assertion, which states a computed result about a single subject. The family binds to NIP-85's subject-type convention by the +10 rule, so the kind's last digit denotes the member type and a reader never has to inspect the tags to know what a list contains: 30392 (= 30382 + 10) pubkeys `p` UserTrustedListEvent 30393 (= 30383 + 10) events `e` EventTrustedListEvent 30394 (= 30384 + 10) addressable events `a` AddressableTrustedListEvent 30395 (= 30385 + 10) external ids `i` ExternalIdTrustedListEvent All four extend TrustedListEvent, which carries what the family shares: the list identity in `d`, the title/metric labels, the observer / source-tag / cutoff / min-rank provenance, the completeness signal, the retraction marker and the optional JSON echo of the membership in `content`. members() is narrowed per kind but always satisfies TrustedListMemberTag, so kind-agnostic readers can take memberValue and score without branching. Notable semantics: - Member tags are [<tag>, <value>, <hint>, <score>] for every kind, so a publisher with a score but no relay hint pads index 2 with an empty string (as the reference publisher does). On `e` members index 3 is the score, not a NIP-10 marker. - isTruncated() keys off the *presence* of the `truncated` tag, since its absence is what promises the list is exhaustive; a tag with a missing or unparseable total still reads as incomplete. - Single-letter tags that are not the kind's member tag are relay-filterable discovery metadata, read through aboutAddresses()/aboutPubKeys() and never through members(). Package layout and tag classes follow the nip88Polls structure. Kinds are registered in EventFactory and KindNames; the package README documents the wire format. Covered by tests over the reference pinned-tag list event. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011f6dt4Zo3g8TAayhCU4tTD
This commit is contained in:
+107
@@ -0,0 +1,107 @@
|
||||
# Trusted Lists (kinds 30392-30395)
|
||||
|
||||
A **Trusted List** is an addressable event that publishes a curated set of
|
||||
**members** computed under a point of view — "the pubkeys trusted-tagged with
|
||||
tag X under observer O", "the notes trusted-tagged with X", "the tags that
|
||||
apply to events". It is the *aggregate* analog of a
|
||||
[NIP-85](https://nips.nostr.com/85) Trusted Assertion: where an assertion
|
||||
states a computed result about **one** subject, a list enumerates **many**
|
||||
members of one type.
|
||||
|
||||
This is a pre-NIP wire format drafted by Tapestry
|
||||
([spec](https://github.com/nous-clawds4/tapestry/blob/main/protocols/drafts/trusted-lists.md)).
|
||||
It does not modify NIP-85; it defines the list analog and binds its kinds to
|
||||
NIP-85's subject-type convention.
|
||||
|
||||
## Kinds — the `+10` rule
|
||||
|
||||
NIP-85 encodes the *subject* type in the last digit of `3038x`. Trusted Lists
|
||||
mirror that with `TL kind = TA kind + 10`, so the last digit denotes the
|
||||
**member** type and a reader can tell what a list contains from the kind alone,
|
||||
without inspecting the tags.
|
||||
|
||||
| Kind | = TA + 10 | Members | Member tag | Quartz class |
|
||||
|---|---|---|---|---|
|
||||
| 30392 | 30382 | pubkeys | `p` | `users.UserTrustedListEvent` |
|
||||
| 30393 | 30383 | events | `e` | `events.EventTrustedListEvent` |
|
||||
| 30394 | 30384 | addressable events | `a` | `addressables.AddressableTrustedListEvent` |
|
||||
| 30395 | 30385 | external identifiers (NIP-73) | `i` | `externalIds.ExternalIdTrustedListEvent` |
|
||||
|
||||
A list of addressable events therefore **must** be 30394. Publishing
|
||||
a-coordinate members on 30393 (the event-id kind) would tell a conformant
|
||||
reader "these are event ids" when they are coordinates — a category error that
|
||||
breaks kind-keyed dispatch and federation.
|
||||
|
||||
All four extend `TrustedListEvent`, which carries everything the family shares.
|
||||
`members()` is narrowed per kind but always satisfies `TrustedListMemberTag`,
|
||||
so a kind-agnostic reader can take `memberValue` and `score` without branching.
|
||||
|
||||
## Wire shape
|
||||
|
||||
```json
|
||||
{
|
||||
"kind": 30392,
|
||||
"tags": [
|
||||
["d", "tl-pin-2efaa715-e5272de9-podcaster"],
|
||||
["title", "Podcaster"],
|
||||
["metric", "pinned-tag-membership"],
|
||||
["observer", "2efaa715…"],
|
||||
["source-tag", "2f6a8652…", "e5272de9…", "podcaster"],
|
||||
["cutoff", "1"],
|
||||
["min-rank", "2"],
|
||||
["p", "b83a28b7…", "", "99"]
|
||||
],
|
||||
"content": "{\"members\":[{\"pubkey\":\"b83a28b7…\",\"endorsements\":4,\"disputes\":0,\"score\":99}]}"
|
||||
}
|
||||
```
|
||||
|
||||
- `d` identifies the **list**, not a subject, so it replaces in place as it is
|
||||
recomputed (`listId()`).
|
||||
- `title` / `metric` are the label and the name of the computation.
|
||||
- Member tags carry `[<tag>, <value>, <hint>, <score>]`. The score sits at
|
||||
index 3 for every kind in the family, so a publisher with a score but no
|
||||
relay hint pads index 2 with an empty string — as in the `p` tag above.
|
||||
On `e` members index 3 is the score, **not** a NIP-10 marker: these lists
|
||||
enumerate membership, they do not thread.
|
||||
- `observer`, `source-tag`, `cutoff`, `min-rank` are provenance.
|
||||
- Single-letter tags that are *not* the kind's member tag are
|
||||
relay-filterable **discovery** metadata — what the list is about — and are
|
||||
read through `aboutAddresses()` / `aboutPubKeys()`, never through
|
||||
`members()`. The pinned-tag note list (30393) carries
|
||||
`["a", "39999:<tagAuthor>:<slug>"]` so consumers can find every note list for
|
||||
a tag (`{"kinds":[30393],"#a":[coord]}`) and `["p", <observer>]` to find
|
||||
every note list for an observer.
|
||||
- `content` is an optional JSON echo of the members with their computed values
|
||||
(`contentEcho()` → `TrustedListContent`).
|
||||
|
||||
## Completeness and retraction
|
||||
|
||||
A list an integrator relies on must be complete, or say that it isn't. The
|
||||
**absence** of a `truncated` tag means the membership is
|
||||
authoritative-complete; its **presence** means the list is not exhaustive and
|
||||
its value is the true total. `isTruncated()` keys off presence, so a
|
||||
`truncated` tag with a missing or unparseable total still reads as incomplete;
|
||||
`truncatedTotal()` returns the total when it parses. The content echo mirrors
|
||||
this with `partial` + `total`.
|
||||
|
||||
A list that no longer has members, or that is migrating off a kind, is replaced
|
||||
in place by an empty-membership event carrying `["status", "retracted"]`
|
||||
(`isRetracted()`) rather than being deleted.
|
||||
|
||||
## Building
|
||||
|
||||
```kotlin
|
||||
val template =
|
||||
UserTrustedListEvent.build(
|
||||
listId = "tl-pin-2efaa715-e5272de9-podcaster",
|
||||
members = listOf(PubKeyMemberTag(authorHex, score = 99)),
|
||||
content = TrustedListContent(listOf(TrustedListContentMember(pubkey = authorHex, score = 99))).toContent(),
|
||||
) {
|
||||
title("Podcaster")
|
||||
metric("pinned-tag-membership")
|
||||
observer(observerHex)
|
||||
sourceTag(tagEventId, tagAuthorHex, "podcaster")
|
||||
cutoff(1)
|
||||
minRank(2)
|
||||
}
|
||||
```
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.trustedLists
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.CutoffTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.ListStatus
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.MetricTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.MinRankTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.ObserverTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.SourceTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.StatusTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.TruncatedTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
|
||||
|
||||
fun <T : TrustedListEvent> TagArrayBuilder<T>.title(title: String) = addUnique(TitleTag.assemble(title))
|
||||
|
||||
fun <T : TrustedListEvent> TagArrayBuilder<T>.metric(metric: String) = addUnique(MetricTag.assemble(metric))
|
||||
|
||||
fun <T : TrustedListEvent> TagArrayBuilder<T>.observer(observer: HexKey) = addUnique(ObserverTag.assemble(observer))
|
||||
|
||||
fun <T : TrustedListEvent> TagArrayBuilder<T>.sourceTag(source: SourceTag) = addUnique(source.toTagArray())
|
||||
|
||||
fun <T : TrustedListEvent> TagArrayBuilder<T>.sourceTag(
|
||||
eventId: HexKey,
|
||||
author: HexKey? = null,
|
||||
slug: String? = null,
|
||||
) = addUnique(SourceTag.assemble(eventId, author, slug))
|
||||
|
||||
fun <T : TrustedListEvent> TagArrayBuilder<T>.cutoff(cutoff: Int) = addUnique(CutoffTag.assemble(cutoff))
|
||||
|
||||
fun <T : TrustedListEvent> TagArrayBuilder<T>.minRank(minRank: Int) = addUnique(MinRankTag.assemble(minRank))
|
||||
|
||||
/**
|
||||
* Marks the membership as *not* exhaustive, carrying the true [total]. Only
|
||||
* add it when the list had to be cut: its absence is what tells consumers the
|
||||
* list is complete.
|
||||
*/
|
||||
fun <T : TrustedListEvent> TagArrayBuilder<T>.truncated(total: Int) = addUnique(TruncatedTag.assemble(total))
|
||||
|
||||
fun <T : TrustedListEvent> TagArrayBuilder<T>.status(status: ListStatus) = addUnique(StatusTag.assemble(status))
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.trustedLists
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.CutoffTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.MetricTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.MinRankTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.ObserverTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.SourceTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.StatusTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.TruncatedTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.core.fastAny
|
||||
import com.vitorpamplona.quartz.nip01Core.core.fastFirstNotNullOfOrNull
|
||||
import com.vitorpamplona.quartz.nip51Lists.tags.TitleTag
|
||||
|
||||
fun TagArray.title() = fastFirstNotNullOfOrNull(TitleTag::parse)
|
||||
|
||||
fun TagArray.metric() = fastFirstNotNullOfOrNull(MetricTag::parse)
|
||||
|
||||
fun TagArray.observer() = fastFirstNotNullOfOrNull(ObserverTag::parse)
|
||||
|
||||
fun TagArray.sourceTag() = fastFirstNotNullOfOrNull(SourceTag::parse)
|
||||
|
||||
fun TagArray.cutoff() = fastFirstNotNullOfOrNull(CutoffTag::parse)
|
||||
|
||||
fun TagArray.minRank() = fastFirstNotNullOfOrNull(MinRankTag::parse)
|
||||
|
||||
fun TagArray.truncatedTotal() = fastFirstNotNullOfOrNull(TruncatedTag::parse)
|
||||
|
||||
fun TagArray.isTruncated() = fastAny(TruncatedTag::isTag)
|
||||
|
||||
fun TagArray.status() = fastFirstNotNullOfOrNull(StatusTag::parse)
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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.trustedLists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.nip01Core.core.JsonMapper
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* The optional JSON echo of the membership carried in `content`, with the
|
||||
* values the publisher computed for each member.
|
||||
*
|
||||
* [partial] mirrors the `truncated` tag: it is only present (with [total])
|
||||
* when the list is not exhaustive.
|
||||
*/
|
||||
@Immutable
|
||||
@Serializable
|
||||
data class TrustedListContent(
|
||||
val members: List<TrustedListContentMember> = emptyList(),
|
||||
val partial: Boolean? = null,
|
||||
val total: Int? = null,
|
||||
) {
|
||||
fun toContent() = assemble(this)
|
||||
|
||||
companion object {
|
||||
fun parse(content: String): TrustedListContent? {
|
||||
if (content.isBlank()) return null
|
||||
return runCatching { JsonMapper.fromJson<TrustedListContent>(content) }.getOrNull()
|
||||
}
|
||||
|
||||
fun assemble(content: TrustedListContent) = JsonMapper.toJson(content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One member of the content echo. The member value is carried under the key
|
||||
* that matches the list's member type -- `pubkey` on 30392, `id` on 30393,
|
||||
* `address` on 30394 and `i` on 30395 -- so exactly one of them is set.
|
||||
* Use [memberValue] to read it without branching on the kind.
|
||||
*/
|
||||
@Immutable
|
||||
@Serializable
|
||||
data class TrustedListContentMember(
|
||||
val pubkey: String? = null,
|
||||
val id: String? = null,
|
||||
val address: String? = null,
|
||||
val i: String? = null,
|
||||
val endorsements: Int? = null,
|
||||
val disputes: Int? = null,
|
||||
val score: Int? = null,
|
||||
) {
|
||||
fun memberValue() = pubkey ?: id ?: address ?: i
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.trustedLists
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.ListStatus
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.TrustedListMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
|
||||
/**
|
||||
* Base of the Tapestry Trusted List family: an addressable event that
|
||||
* publishes a curated set of members computed under a point of view.
|
||||
*
|
||||
* Where a NIP-85 Trusted Assertion states a computed result about **one**
|
||||
* subject, a Trusted List enumerates **many** members of one type. The family
|
||||
* binds to NIP-85's subject-type convention by the `+10` rule -- a list kind
|
||||
* is its assertion kind plus ten -- so the last digit denotes the member type
|
||||
* on both sides and a reader can tell what a list contains from the kind
|
||||
* alone:
|
||||
*
|
||||
* | List kind | Assertion kind | Member type | Member tag |
|
||||
* |---|---|---|---|
|
||||
* | 30392 | 30382 | pubkeys | `p` |
|
||||
* | 30393 | 30383 | events | `e` |
|
||||
* | 30394 | 30384 | addressable events | `a` |
|
||||
* | 30395 | 30385 | external identifiers (NIP-73) | `i` |
|
||||
*
|
||||
* The `d` tag identifies the *list*, not a single subject, so the list
|
||||
* replaces in place as it is recomputed.
|
||||
*/
|
||||
@Immutable
|
||||
abstract class TrustedListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
kind: Int,
|
||||
tags: TagArray,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
/** The addressable identity of this list. Deterministic per list. */
|
||||
fun listId() = dTag()
|
||||
|
||||
fun title() = tags.title()
|
||||
|
||||
fun metric() = tags.metric()
|
||||
|
||||
fun observer() = tags.observer()
|
||||
|
||||
fun sourceTag() = tags.sourceTag()
|
||||
|
||||
fun cutoff() = tags.cutoff()
|
||||
|
||||
fun minRank() = tags.minRank()
|
||||
|
||||
/**
|
||||
* The members of this list, in the tag this kind's last digit denotes.
|
||||
* Subclasses narrow the return type to their own member tag.
|
||||
*/
|
||||
abstract fun members(): List<TrustedListMemberTag>
|
||||
|
||||
fun memberValues() = members().map { it.memberValue }
|
||||
|
||||
fun memberCount() = members().size
|
||||
|
||||
/**
|
||||
* True when the publisher signalled that it could not carry the full
|
||||
* membership. A list without the `truncated` tag is authoritative-complete;
|
||||
* a truncated one is a cue to reconcile from the raw source events.
|
||||
*/
|
||||
fun isTruncated() = tags.isTruncated()
|
||||
|
||||
/** The true total member count, when the list is truncated. */
|
||||
fun truncatedTotal() = tags.truncatedTotal()
|
||||
|
||||
fun status() = tags.status()
|
||||
|
||||
/**
|
||||
* Retracted lists are empty-membership replacements published in place of
|
||||
* a list that no longer has members or was migrated to another kind.
|
||||
*/
|
||||
fun isRetracted() = status() == ListStatus.RETRACTED
|
||||
|
||||
/** The optional JSON echo of the membership carried in `content`. */
|
||||
fun contentEcho() = TrustedListContent.parse(content)
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.trustedLists.addressables
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.TrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.addressables.tags.AddressMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* A Trusted List whose members are **addressable events**, by a-coordinate --
|
||||
* the list analog of the NIP-85 kind-30384 addressable assertion (30384 + 10).
|
||||
*
|
||||
* Example: the tag-applicability lists, whose members are tag coordinates.
|
||||
*/
|
||||
@Immutable
|
||||
class AddressableTrustedListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : TrustedListEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
AddressHintProvider,
|
||||
PubKeyHintProvider {
|
||||
override fun members(): List<AddressMemberTag> = tags.members()
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(AddressMemberTag::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(AddressMemberTag::parseAddressId)
|
||||
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
/** What this list is about, for relay-side discovery. Never its members. */
|
||||
fun aboutPubKeys() = tags.aboutPubKeys()
|
||||
|
||||
companion object {
|
||||
const val KIND = 30394
|
||||
|
||||
fun build(
|
||||
listId: String,
|
||||
members: List<AddressMemberTag> = emptyList(),
|
||||
content: String = "",
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<AddressableTrustedListEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
dTag(listId)
|
||||
members(members)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.trustedLists.addressables
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.addressables.tags.AddressMemberTag
|
||||
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.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
|
||||
fun TagArrayBuilder<AddressableTrustedListEvent>.member(
|
||||
address: String,
|
||||
relayHint: NormalizedRelayUrl? = null,
|
||||
score: Int? = null,
|
||||
) = add(AddressMemberTag.assemble(address, relayHint, score))
|
||||
|
||||
fun TagArrayBuilder<AddressableTrustedListEvent>.member(
|
||||
address: Address,
|
||||
relayHint: NormalizedRelayUrl? = null,
|
||||
score: Int? = null,
|
||||
) = add(AddressMemberTag.assemble(address.toValue(), relayHint, score))
|
||||
|
||||
fun TagArrayBuilder<AddressableTrustedListEvent>.member(member: AddressMemberTag) = add(AddressMemberTag.assemble(member))
|
||||
|
||||
fun TagArrayBuilder<AddressableTrustedListEvent>.members(members: List<AddressMemberTag>) = addAll(AddressMemberTag.assemble(members))
|
||||
|
||||
fun TagArrayBuilder<AddressableTrustedListEvent>.aboutPubKey(pubKey: PTag) = addUniqueValueIfNew(pubKey.toTagArray())
|
||||
|
||||
fun TagArrayBuilder<AddressableTrustedListEvent>.aboutPubKey(
|
||||
pubKey: HexKey,
|
||||
relayHint: NormalizedRelayUrl? = null,
|
||||
) = addUniqueValueIfNew(PTag.assemble(pubKey, relayHint))
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.trustedLists.addressables
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.addressables.tags.AddressMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
|
||||
fun TagArray.members() = mapNotNull(AddressMemberTag::parse)
|
||||
|
||||
/**
|
||||
* The optional relay-filterable discovery tags: what this list is *about*,
|
||||
* as opposed to its members. On kind 30394 the members are the `a` tags, so
|
||||
* only `p` is free to carry discovery metadata.
|
||||
*/
|
||||
fun TagArray.aboutPubKeys() = mapNotNull(PTag::parse)
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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.trustedLists.addressables.tags
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.TrustedListMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.AddressSerializer
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
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.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* An addressable-event member of a kind-30394 Trusted List:
|
||||
* `["a", <kind:pubkey:d>, <relay-hint>, <score>]`.
|
||||
*
|
||||
* A-coordinate members belong on 30394 and never on 30393: publishing them on
|
||||
* the event-id kind would tell a conformant reader "these are event ids" when
|
||||
* they are coordinates, breaking kind-keyed dispatch.
|
||||
*/
|
||||
@Immutable
|
||||
data class AddressMemberTag(
|
||||
val address: String,
|
||||
val relayHint: NormalizedRelayUrl? = null,
|
||||
override val score: Int? = null,
|
||||
) : TrustedListMemberTag {
|
||||
override val memberValue: String get() = address
|
||||
|
||||
fun toAddress(): Address? = AddressSerializer.parse(address)
|
||||
|
||||
fun toTagArray() = assemble(address, relayHint, score)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "a"
|
||||
|
||||
fun isTag(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun isTagged(
|
||||
tag: Tag,
|
||||
address: String,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == address
|
||||
|
||||
fun parse(tag: Tag): AddressMemberTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
return AddressMemberTag(tag[1], parseHint(tag), parseScore(tag))
|
||||
}
|
||||
|
||||
fun parseAddressId(tag: Tag): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun parseAddress(tag: Tag): Address? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return AddressSerializer.parse(tag[1])
|
||||
}
|
||||
|
||||
fun parseAsHint(tag: Tag): AddressHint? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
val hint = parseHint(tag)
|
||||
|
||||
ensure(hint != null) { return null }
|
||||
|
||||
return AddressHint(tag[1], hint)
|
||||
}
|
||||
|
||||
private fun parseHint(tag: Tag) = tag.getOrNull(2)?.takeIf { it.isNotEmpty() }?.let { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||
|
||||
private fun parseScore(tag: Tag) = tag.getOrNull(3)?.toIntOrNull()
|
||||
|
||||
fun assemble(
|
||||
address: String,
|
||||
relayHint: NormalizedRelayUrl?,
|
||||
score: Int?,
|
||||
) = arrayOfNotNull(TAG_NAME, address, relayHint?.url, score?.toString())
|
||||
|
||||
fun assemble(member: AddressMemberTag) = assemble(member.address, member.relayHint, member.score)
|
||||
|
||||
fun assemble(members: List<AddressMemberTag>) = members.map { assemble(it) }
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.trustedLists.events
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.TrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.events.tags.EventMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.eventTemplate
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.dTag.dTag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* A Trusted List whose members are **events**, by id -- the list analog of the
|
||||
* NIP-85 kind-30383 event assertion (30383 + 10).
|
||||
*
|
||||
* Example: the notes trusted-tagged with a given tag under one observer. Its
|
||||
* `a` and `p` tags are discovery metadata (the tag coordinate and the
|
||||
* observer), not members.
|
||||
*/
|
||||
@Immutable
|
||||
class EventTrustedListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : TrustedListEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
EventHintProvider,
|
||||
AddressHintProvider,
|
||||
PubKeyHintProvider {
|
||||
override fun members(): List<EventMemberTag> = tags.members()
|
||||
|
||||
override fun eventHints() = tags.mapNotNull(EventMemberTag::parseAsHint)
|
||||
|
||||
override fun linkedEventIds() = tags.mapNotNull(EventMemberTag::parseId)
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
|
||||
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
/** What this list is about, for relay-side discovery. Never its members. */
|
||||
fun aboutAddresses() = tags.aboutAddresses()
|
||||
|
||||
fun aboutPubKeys() = tags.aboutPubKeys()
|
||||
|
||||
companion object {
|
||||
const val KIND = 30393
|
||||
|
||||
fun build(
|
||||
listId: String,
|
||||
members: List<EventMemberTag> = emptyList(),
|
||||
content: String = "",
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<EventTrustedListEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
dTag(listId)
|
||||
members(members)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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.trustedLists.events
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.events.tags.EventMemberTag
|
||||
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.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
|
||||
fun TagArrayBuilder<EventTrustedListEvent>.member(
|
||||
eventId: HexKey,
|
||||
relayHint: NormalizedRelayUrl? = null,
|
||||
score: Int? = null,
|
||||
) = add(EventMemberTag.assemble(eventId, relayHint, score))
|
||||
|
||||
fun TagArrayBuilder<EventTrustedListEvent>.member(member: EventMemberTag) = add(EventMemberTag.assemble(member))
|
||||
|
||||
fun TagArrayBuilder<EventTrustedListEvent>.members(members: List<EventMemberTag>) = addAll(EventMemberTag.assemble(members))
|
||||
|
||||
fun TagArrayBuilder<EventTrustedListEvent>.aboutAddress(address: ATag) = addUniqueValueIfNew(address.toATagArray())
|
||||
|
||||
fun TagArrayBuilder<EventTrustedListEvent>.aboutPubKey(pubKey: PTag) = addUniqueValueIfNew(pubKey.toTagArray())
|
||||
|
||||
fun TagArrayBuilder<EventTrustedListEvent>.aboutPubKey(
|
||||
pubKey: HexKey,
|
||||
relayHint: NormalizedRelayUrl? = null,
|
||||
) = addUniqueValueIfNew(PTag.assemble(pubKey, relayHint))
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.trustedLists.events
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.events.tags.EventMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
|
||||
fun TagArray.members() = mapNotNull(EventMemberTag::parse)
|
||||
|
||||
/**
|
||||
* The optional relay-filterable discovery tags: what this list is *about*,
|
||||
* as opposed to its members. On kind 30393 the members are the `e` tags, so
|
||||
* both `a` and `p` are free to carry discovery metadata -- the pinned-tag note
|
||||
* list uses `a` for the tag coordinate and `p` for the observer, so consumers
|
||||
* can find every note list for a tag (`#a`) or for an observer (`#p`).
|
||||
*/
|
||||
fun TagArray.aboutAddresses() = mapNotNull(ATag::parse)
|
||||
|
||||
fun TagArray.aboutPubKeys() = mapNotNull(PTag::parse)
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.trustedLists.events.tags
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.TrustedListMemberTag
|
||||
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.nip01Core.hints.types.EventIdHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* An event member of a kind-30393 Trusted List:
|
||||
* `["e", <event-id>, <relay-hint>, <score>]`.
|
||||
*
|
||||
* Index 3 is the score across the whole family, not a NIP-10 marker: these
|
||||
* lists enumerate membership, they do not thread.
|
||||
*/
|
||||
@Immutable
|
||||
data class EventMemberTag(
|
||||
val eventId: HexKey,
|
||||
val relayHint: NormalizedRelayUrl? = null,
|
||||
override val score: Int? = null,
|
||||
) : TrustedListMemberTag {
|
||||
override val memberValue: String get() = eventId
|
||||
|
||||
fun toTagArray() = assemble(eventId, relayHint, score)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "e"
|
||||
|
||||
fun isTag(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64
|
||||
|
||||
fun isTagged(
|
||||
tag: Tag,
|
||||
eventId: HexKey,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == eventId
|
||||
|
||||
fun parse(tag: Tag): EventMemberTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
return EventMemberTag(tag[1], parseHint(tag), parseScore(tag))
|
||||
}
|
||||
|
||||
fun parseId(tag: Tag): HexKey? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun parseAsHint(tag: Tag): EventIdHint? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
val hint = parseHint(tag)
|
||||
|
||||
ensure(hint != null) { return null }
|
||||
|
||||
return EventIdHint(tag[1], hint)
|
||||
}
|
||||
|
||||
private fun parseHint(tag: Tag) = tag.getOrNull(2)?.takeIf { it.isNotEmpty() }?.let { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||
|
||||
private fun parseScore(tag: Tag) = tag.getOrNull(3)?.toIntOrNull()
|
||||
|
||||
fun assemble(
|
||||
eventId: HexKey,
|
||||
relayHint: NormalizedRelayUrl?,
|
||||
score: Int?,
|
||||
) = arrayOfNotNull(TAG_NAME, eventId, relayHint?.url, score?.toString())
|
||||
|
||||
fun assemble(member: EventMemberTag) = assemble(member.eventId, member.relayHint, member.score)
|
||||
|
||||
fun assemble(members: List<EventMemberTag>) = members.map { assemble(it) }
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.trustedLists.externalIds
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.TrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.externalIds.tags.ExternalIdMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
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.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* A Trusted List whose members are **external identifiers** (NIP-73) -- the
|
||||
* list analog of the NIP-85 kind-30385 external-id assertion (30385 + 10).
|
||||
*/
|
||||
@Immutable
|
||||
class ExternalIdTrustedListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : TrustedListEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
AddressHintProvider,
|
||||
PubKeyHintProvider {
|
||||
override fun members(): List<ExternalIdMemberTag> = tags.members()
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
|
||||
|
||||
override fun pubKeyHints() = tags.mapNotNull(PTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PTag::parseKey)
|
||||
|
||||
/** What this list is about, for relay-side discovery. Never its members. */
|
||||
fun aboutAddresses() = tags.aboutAddresses()
|
||||
|
||||
fun aboutPubKeys() = tags.aboutPubKeys()
|
||||
|
||||
companion object {
|
||||
const val KIND = 30395
|
||||
|
||||
fun build(
|
||||
listId: String,
|
||||
members: List<ExternalIdMemberTag> = emptyList(),
|
||||
content: String = "",
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<ExternalIdTrustedListEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
dTag(listId)
|
||||
members(members)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -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.experimental.trustedLists.externalIds
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.externalIds.tags.ExternalIdMemberTag
|
||||
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.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||
|
||||
fun TagArrayBuilder<ExternalIdTrustedListEvent>.member(
|
||||
externalId: String,
|
||||
hint: String? = null,
|
||||
score: Int? = null,
|
||||
) = add(ExternalIdMemberTag.assemble(externalId, hint, score))
|
||||
|
||||
fun TagArrayBuilder<ExternalIdTrustedListEvent>.member(
|
||||
externalId: ExternalId,
|
||||
score: Int? = null,
|
||||
) = add(ExternalIdMemberTag.assemble(externalId, score))
|
||||
|
||||
fun TagArrayBuilder<ExternalIdTrustedListEvent>.member(member: ExternalIdMemberTag) = add(ExternalIdMemberTag.assemble(member))
|
||||
|
||||
fun TagArrayBuilder<ExternalIdTrustedListEvent>.members(members: List<ExternalIdMemberTag>) = addAll(ExternalIdMemberTag.assemble(members))
|
||||
|
||||
fun TagArrayBuilder<ExternalIdTrustedListEvent>.aboutAddress(address: ATag) = addUniqueValueIfNew(address.toATagArray())
|
||||
|
||||
fun TagArrayBuilder<ExternalIdTrustedListEvent>.aboutPubKey(pubKey: PTag) = addUniqueValueIfNew(pubKey.toTagArray())
|
||||
|
||||
fun TagArrayBuilder<ExternalIdTrustedListEvent>.aboutPubKey(
|
||||
pubKey: HexKey,
|
||||
relayHint: NormalizedRelayUrl? = null,
|
||||
) = addUniqueValueIfNew(PTag.assemble(pubKey, relayHint))
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.trustedLists.externalIds
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.externalIds.tags.ExternalIdMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
|
||||
fun TagArray.members() = mapNotNull(ExternalIdMemberTag::parse)
|
||||
|
||||
/**
|
||||
* The optional relay-filterable discovery tags: what this list is *about*,
|
||||
* as opposed to its members. On kind 30395 the members are the `i` tags, so
|
||||
* both `a` and `p` are free to carry discovery metadata.
|
||||
*/
|
||||
fun TagArray.aboutAddresses() = mapNotNull(ATag::parse)
|
||||
|
||||
fun TagArray.aboutPubKeys() = mapNotNull(PTag::parse)
|
||||
+92
@@ -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.experimental.trustedLists.externalIds.tags
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.TrustedListMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.ExternalId
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* An external-identifier member of a kind-30395 Trusted List:
|
||||
* `["i", <i-tag>, <url-hint>, <score>]`.
|
||||
*
|
||||
* Index 2 is NIP-73's URL hint rather than a relay hint, so these members
|
||||
* carry no relay information for the hint indexer.
|
||||
*/
|
||||
@Immutable
|
||||
data class ExternalIdMemberTag(
|
||||
val externalId: String,
|
||||
val hint: String? = null,
|
||||
override val score: Int? = null,
|
||||
) : TrustedListMemberTag {
|
||||
override val memberValue: String get() = externalId
|
||||
|
||||
fun toTagArray() = assemble(externalId, hint, score)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "i"
|
||||
|
||||
fun isTag(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun isTagged(
|
||||
tag: Tag,
|
||||
externalId: String,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == externalId
|
||||
|
||||
fun parse(tag: Tag): ExternalIdMemberTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
return ExternalIdMemberTag(
|
||||
tag[1],
|
||||
tag.getOrNull(2)?.takeIf { it.isNotEmpty() },
|
||||
tag.getOrNull(3)?.toIntOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
fun parseId(tag: Tag): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
externalId: String,
|
||||
hint: String?,
|
||||
score: Int?,
|
||||
) = arrayOfNotNull(TAG_NAME, externalId, hint, score?.toString())
|
||||
|
||||
fun assemble(member: ExternalIdMemberTag) = assemble(member.externalId, member.hint, member.score)
|
||||
|
||||
fun assemble(members: List<ExternalIdMemberTag>) = members.map { assemble(it) }
|
||||
|
||||
fun assemble(
|
||||
externalId: ExternalId,
|
||||
score: Int? = null,
|
||||
) = assemble(externalId.toScope(), externalId.hint(), score)
|
||||
}
|
||||
}
|
||||
+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.experimental.trustedLists.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/** Minimum number of endorsements a member needed to make the list. */
|
||||
class CutoffTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "cutoff"
|
||||
|
||||
fun isTag(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Tag): Int? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toIntOrNull()
|
||||
}
|
||||
|
||||
fun assemble(cutoff: Int) = arrayOf(TAG_NAME, cutoff.toString())
|
||||
}
|
||||
}
|
||||
+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.experimental.trustedLists.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* Names the computation that produced the list, e.g. `pinned-tag-membership`,
|
||||
* `pinned-tag-notes` or `tag-applicability`.
|
||||
*/
|
||||
class MetricTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "metric"
|
||||
|
||||
fun isTag(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Tag): String? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(metric: String) = arrayOf(TAG_NAME, metric)
|
||||
}
|
||||
}
|
||||
+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.experimental.trustedLists.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/** Minimum rank an endorser needed for their endorsement to count. */
|
||||
class MinRankTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "min-rank"
|
||||
|
||||
fun isTag(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Tag): Int? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toIntOrNull()
|
||||
}
|
||||
|
||||
fun assemble(minRank: Int) = arrayOf(TAG_NAME, minRank.toString())
|
||||
}
|
||||
}
|
||||
+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.experimental.trustedLists.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.ensure
|
||||
|
||||
/**
|
||||
* The point of view the list was computed under. This is provenance, not a
|
||||
* member and not a notification: it never uses the `p` tag name, which on
|
||||
* kind 30392 carries the members themselves.
|
||||
*/
|
||||
class ObserverTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "observer"
|
||||
|
||||
fun isTag(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64
|
||||
|
||||
fun parse(tag: Tag): HexKey? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun assemble(observer: HexKey) = arrayOf(TAG_NAME, observer)
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.trustedLists.tags
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* The tag definition the membership was computed from:
|
||||
* `["source-tag", <eventId>, <author>, <slug>]`.
|
||||
*/
|
||||
@Immutable
|
||||
data class SourceTag(
|
||||
val eventId: HexKey,
|
||||
val author: HexKey? = null,
|
||||
val slug: String? = null,
|
||||
) {
|
||||
fun toTagArray() = assemble(eventId, author, slug)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "source-tag"
|
||||
|
||||
fun isTag(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64
|
||||
|
||||
fun parse(tag: Tag): SourceTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return SourceTag(
|
||||
tag[1],
|
||||
tag.getOrNull(2)?.ifEmpty { null },
|
||||
tag.getOrNull(3)?.ifEmpty { null },
|
||||
)
|
||||
}
|
||||
|
||||
fun assemble(
|
||||
eventId: HexKey,
|
||||
author: HexKey?,
|
||||
slug: String?,
|
||||
) = arrayOfNotNull(TAG_NAME, eventId, author, slug)
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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.trustedLists.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
enum class ListStatus(
|
||||
val code: String,
|
||||
) {
|
||||
RETRACTED("retracted"),
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the retraction convention: a list that no longer has members, or that
|
||||
* is being migrated off a kind, is replaced in place by an empty-membership
|
||||
* event carrying `["status", "retracted"]` rather than being deleted.
|
||||
*/
|
||||
class StatusTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "status"
|
||||
|
||||
fun isTag(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].isNotEmpty()
|
||||
|
||||
fun parse(tag: Tag): ListStatus? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
|
||||
return when (tag[1]) {
|
||||
ListStatus.RETRACTED.code -> ListStatus.RETRACTED
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun assemble(status: ListStatus) = arrayOf(TAG_NAME, status.code)
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.trustedLists.tags
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Tag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.has
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* The partial signal. Its **presence** means the list is not exhaustive; its
|
||||
* absence means the publisher considers the membership complete. The value is
|
||||
* the true total member count.
|
||||
*
|
||||
* Readers must key completeness off [isTag], not off [parse]: a `truncated`
|
||||
* tag with a missing or unparseable total still means "not exhaustive".
|
||||
*/
|
||||
class TruncatedTag {
|
||||
companion object {
|
||||
const val TAG_NAME = "truncated"
|
||||
|
||||
fun isTag(tag: Tag) = tag.isNotEmpty() && tag[0] == TAG_NAME
|
||||
|
||||
fun parse(tag: Tag): Int? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].isNotEmpty()) { return null }
|
||||
return tag[1].toIntOrNull()
|
||||
}
|
||||
|
||||
fun assemble(total: Int) = arrayOf(TAG_NAME, total.toString())
|
||||
}
|
||||
}
|
||||
+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.experimental.trustedLists.tags
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
/**
|
||||
* Kind-agnostic view of a Trusted List member.
|
||||
*
|
||||
* Every kind in the family carries its members in the tag its last digit
|
||||
* denotes (`p`/`e`/`a`/`i`), all with the same layout:
|
||||
* `[<tagName>, <memberValue>, <hint>, <score>]`. A reader that only needs the
|
||||
* membership can dispatch on this interface instead of on the kind.
|
||||
*/
|
||||
@Stable
|
||||
interface TrustedListMemberTag {
|
||||
/** The pubkey, event id, a-coordinate or external id of this member. */
|
||||
val memberValue: String
|
||||
|
||||
/** The computed score the publisher assigned to this member, if any. */
|
||||
val score: Int?
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.quartz.experimental.trustedLists.users
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.users.tags.PubKeyMemberTag
|
||||
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.nip01Core.tags.aTag.ATag
|
||||
|
||||
fun TagArrayBuilder<UserTrustedListEvent>.member(
|
||||
pubKey: HexKey,
|
||||
relayHint: NormalizedRelayUrl? = null,
|
||||
score: Int? = null,
|
||||
) = add(PubKeyMemberTag.assemble(pubKey, relayHint, score))
|
||||
|
||||
fun TagArrayBuilder<UserTrustedListEvent>.member(member: PubKeyMemberTag) = add(PubKeyMemberTag.assemble(member))
|
||||
|
||||
fun TagArrayBuilder<UserTrustedListEvent>.members(members: List<PubKeyMemberTag>) = addAll(PubKeyMemberTag.assemble(members))
|
||||
|
||||
fun TagArrayBuilder<UserTrustedListEvent>.aboutAddress(address: ATag) = addUniqueValueIfNew(address.toATagArray())
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.trustedLists.users
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.users.tags.PubKeyMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArray
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
|
||||
fun TagArray.members() = mapNotNull(PubKeyMemberTag::parse)
|
||||
|
||||
/**
|
||||
* The optional relay-filterable discovery tags: what this list is *about*,
|
||||
* as opposed to its members. On kind 30392 the members are the `p` tags, so
|
||||
* only `a` is free to carry discovery metadata.
|
||||
*/
|
||||
fun TagArray.aboutAddresses() = mapNotNull(ATag::parse)
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.trustedLists.users
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.TrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.users.tags.PubKeyMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.core.TagArrayBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.AddressHintProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.PubKeyHintProvider
|
||||
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.utils.TimeUtils
|
||||
|
||||
/**
|
||||
* A Trusted List whose members are **pubkeys** -- the list analog of the
|
||||
* NIP-85 kind-30382 contact card (30382 + 10).
|
||||
*
|
||||
* Example: the pubkeys trusted-tagged with a given tag under one observer.
|
||||
*/
|
||||
@Immutable
|
||||
class UserTrustedListEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: Array<Array<String>>,
|
||||
content: String,
|
||||
sig: HexKey,
|
||||
) : TrustedListEvent(id, pubKey, createdAt, KIND, tags, content, sig),
|
||||
PubKeyHintProvider,
|
||||
AddressHintProvider {
|
||||
override fun members(): List<PubKeyMemberTag> = tags.members()
|
||||
|
||||
override fun pubKeyHints() = tags.mapNotNull(PubKeyMemberTag::parseAsHint)
|
||||
|
||||
override fun linkedPubKeys() = tags.mapNotNull(PubKeyMemberTag::parseKey)
|
||||
|
||||
override fun addressHints() = tags.mapNotNull(ATag::parseAsHint)
|
||||
|
||||
override fun linkedAddressIds() = tags.mapNotNull(ATag::parseAddressId)
|
||||
|
||||
/** What this list is about, for relay-side discovery. Never its members. */
|
||||
fun aboutAddresses() = tags.aboutAddresses()
|
||||
|
||||
companion object {
|
||||
const val KIND = 30392
|
||||
|
||||
fun build(
|
||||
listId: String,
|
||||
members: List<PubKeyMemberTag> = emptyList(),
|
||||
content: String = "",
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
initializer: TagArrayBuilder<UserTrustedListEvent>.() -> Unit = {},
|
||||
) = eventTemplate(KIND, content, createdAt) {
|
||||
dTag(listId)
|
||||
members(members)
|
||||
initializer()
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.trustedLists.users.tags
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.TrustedListMemberTag
|
||||
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.nip01Core.hints.types.PubKeyHint
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PubKeyReferenceTag
|
||||
import com.vitorpamplona.quartz.utils.arrayOfNotNull
|
||||
import com.vitorpamplona.quartz.utils.ensure
|
||||
|
||||
/**
|
||||
* A pubkey member of a kind-30392 Trusted List:
|
||||
* `["p", <pubkey>, <relay-hint>, <score>]`.
|
||||
*
|
||||
* The score sits at index 3 across the whole family, so publishers that have a
|
||||
* score but no relay hint pad index 2 with an empty string.
|
||||
*/
|
||||
@Immutable
|
||||
data class PubKeyMemberTag(
|
||||
override val pubKey: HexKey,
|
||||
override val relayHint: NormalizedRelayUrl? = null,
|
||||
override val score: Int? = null,
|
||||
) : PubKeyReferenceTag,
|
||||
TrustedListMemberTag {
|
||||
override val memberValue: String get() = pubKey
|
||||
|
||||
fun toTagArray() = assemble(pubKey, relayHint, score)
|
||||
|
||||
companion object {
|
||||
const val TAG_NAME = "p"
|
||||
|
||||
fun isTag(tag: Tag) = tag.has(1) && tag[0] == TAG_NAME && tag[1].length == 64
|
||||
|
||||
fun isTagged(
|
||||
tag: Tag,
|
||||
pubKey: HexKey,
|
||||
) = tag.has(1) && tag[0] == TAG_NAME && tag[1] == pubKey
|
||||
|
||||
fun parse(tag: Tag): PubKeyMemberTag? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
return PubKeyMemberTag(tag[1], parseHint(tag), parseScore(tag))
|
||||
}
|
||||
|
||||
fun parseKey(tag: Tag): HexKey? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
return tag[1]
|
||||
}
|
||||
|
||||
fun parseAsHint(tag: Tag): PubKeyHint? {
|
||||
ensure(tag.has(1)) { return null }
|
||||
ensure(tag[0] == TAG_NAME) { return null }
|
||||
ensure(tag[1].length == 64) { return null }
|
||||
|
||||
val hint = parseHint(tag)
|
||||
|
||||
ensure(hint != null) { return null }
|
||||
|
||||
return PubKeyHint(tag[1], hint)
|
||||
}
|
||||
|
||||
private fun parseHint(tag: Tag) = tag.getOrNull(2)?.takeIf { it.isNotEmpty() }?.let { RelayUrlNormalizer.normalizeOrNull(it) }
|
||||
|
||||
private fun parseScore(tag: Tag) = tag.getOrNull(3)?.toIntOrNull()
|
||||
|
||||
fun assemble(
|
||||
pubKey: HexKey,
|
||||
relayHint: NormalizedRelayUrl?,
|
||||
score: Int?,
|
||||
) = arrayOfNotNull(TAG_NAME, pubKey, relayHint?.url, score?.toString())
|
||||
|
||||
fun assemble(member: PubKeyMemberTag) = assemble(member.pubKey, member.relayHint, member.score)
|
||||
|
||||
fun assemble(members: List<PubKeyMemberTag>) = members.map { assemble(it) }
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,10 @@ import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryE
|
||||
import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.addressables.AddressableTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.events.EventTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.externalIds.ExternalIdTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.users.UserTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.feedDefinition.FeedDefinitionEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
@@ -600,6 +604,10 @@ object KindNames {
|
||||
EventAssertionEvent.KIND to KindName("Event Assertion", "85"),
|
||||
AddressableAssertionEvent.KIND to KindName("Addressable Assertion", "85"),
|
||||
ExternalIdAssertionEvent.KIND to KindName("External ID Assertion", "85"),
|
||||
UserTrustedListEvent.KIND to KindName("Trusted List of Pubkeys", null),
|
||||
EventTrustedListEvent.KIND to KindName("Trusted List of Events", null),
|
||||
AddressableTrustedListEvent.KIND to KindName("Trusted List of Addressables", null),
|
||||
ExternalIdTrustedListEvent.KIND to KindName("Trusted List of External IDs", null),
|
||||
KeyPackageEvent.KIND to KindName("MLS KeyPackage", null),
|
||||
GitRepositoryStateEvent.KIND to KindName("Git Repo State", "34"),
|
||||
FeedDefinitionEvent.KIND to KindName("Feed Definition", null),
|
||||
|
||||
@@ -141,6 +141,10 @@ import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryE
|
||||
import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.confirmation.RoadEventConfirmationEvent
|
||||
import com.vitorpamplona.quartz.experimental.roadstr.report.RoadEventReportEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.addressables.AddressableTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.events.EventTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.externalIds.ExternalIdTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.users.UserTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.zapPolls.ZapPollEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageEvent
|
||||
import com.vitorpamplona.quartz.marmot.mip00KeyPackages.KeyPackageRelayListEvent
|
||||
@@ -786,6 +790,10 @@ class EventFactory {
|
||||
EventAssertionEvent.KIND -> EventAssertionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
AddressableAssertionEvent.KIND -> AddressableAssertionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ExternalIdAssertionEvent.KIND -> ExternalIdAssertionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
UserTrustedListEvent.KIND -> UserTrustedListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
EventTrustedListEvent.KIND -> EventTrustedListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
AddressableTrustedListEvent.KIND -> AddressableTrustedListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ExternalIdTrustedListEvent.KIND -> ExternalIdTrustedListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RelayAddMemberEvent.KIND -> RelayAddMemberEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RelayRemoveMemberEvent.KIND -> RelayRemoveMemberEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
RelayMembershipListEvent.KIND -> RelayMembershipListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 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.trustedLists
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.ListStatus
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.users.UserTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.users.tags.PubKeyMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.utils.EventFactory
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TrustedListBuilderTest {
|
||||
private val observer = "2efaa715bbb46dd5be6b7da8d7700266d11674b913b8178addb5c2e63d987331"
|
||||
private val tagEventId = "2f6a8652bde6fb5a974d6e06e4eae3b4f130140fd170b2686a291463f47a7451"
|
||||
private val tagAuthor = "e5272de914bd301755c439b88e6959a43c9d2664831f093c51e9c799a16a102f"
|
||||
private val member = "b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450"
|
||||
|
||||
private fun sign(template: EventTemplate<UserTrustedListEvent>): Event =
|
||||
EventFactory.create<Event>(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "a68dbf561cfe3da1b76f1e65c7d4d9cc116f79921b38a815fd75cb5460b4b599",
|
||||
createdAt = template.createdAt,
|
||||
kind = template.kind,
|
||||
tags = template.tags,
|
||||
content = template.content,
|
||||
sig = "00".repeat(64),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun buildsAPinnedTagPubKeyList() {
|
||||
val content =
|
||||
TrustedListContent(
|
||||
members = listOf(TrustedListContentMember(pubkey = member, endorsements = 4, disputes = 0, score = 99)),
|
||||
)
|
||||
|
||||
val template =
|
||||
UserTrustedListEvent.build(
|
||||
listId = "tl-pin-2efaa715-e5272de9-podcaster",
|
||||
members = listOf(PubKeyMemberTag(member, score = 99)),
|
||||
content = content.toContent(),
|
||||
createdAt = 1_787_253_028L,
|
||||
) {
|
||||
title("Podcaster")
|
||||
metric("pinned-tag-membership")
|
||||
observer(observer)
|
||||
sourceTag(tagEventId, tagAuthor, "podcaster")
|
||||
cutoff(1)
|
||||
minRank(2)
|
||||
}
|
||||
|
||||
val event = sign(template)
|
||||
assertIs<UserTrustedListEvent>(event)
|
||||
|
||||
assertEquals(UserTrustedListEvent.KIND, event.kind)
|
||||
assertEquals("tl-pin-2efaa715-e5272de9-podcaster", event.listId())
|
||||
assertEquals("Podcaster", event.title())
|
||||
assertEquals("pinned-tag-membership", event.metric())
|
||||
assertEquals(observer, event.observer())
|
||||
assertEquals(1, event.cutoff())
|
||||
assertEquals(2, event.minRank())
|
||||
assertEquals("podcaster", event.sourceTag()?.slug)
|
||||
assertEquals(listOf(member), event.memberValues())
|
||||
assertEquals(listOf(99), event.members().map { it.score })
|
||||
assertFalse(event.isTruncated(), "a list with no truncated tag is complete")
|
||||
assertEquals(content, event.contentEcho())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildsATruncatedList() {
|
||||
val template =
|
||||
UserTrustedListEvent.build(
|
||||
listId = "tl-pin-2efaa715-e5272de9-podcaster",
|
||||
members = listOf(PubKeyMemberTag(member, score = 99)),
|
||||
content = TrustedListContent(partial = true, total = 4211).toContent(),
|
||||
createdAt = 1_787_253_028L,
|
||||
) {
|
||||
truncated(4211)
|
||||
}
|
||||
|
||||
val event = sign(template)
|
||||
assertIs<UserTrustedListEvent>(event)
|
||||
|
||||
assertTrue(event.isTruncated())
|
||||
assertEquals(4211, event.truncatedTotal())
|
||||
assertEquals(true, event.contentEcho()?.partial)
|
||||
assertEquals(4211, event.contentEcho()?.total)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun buildsAnEmptyRetraction() {
|
||||
val template =
|
||||
UserTrustedListEvent.build(
|
||||
listId = "tl-pin-2efaa715-e5272de9-podcaster",
|
||||
createdAt = 1_787_253_028L,
|
||||
) {
|
||||
status(ListStatus.RETRACTED)
|
||||
}
|
||||
|
||||
val event = sign(template)
|
||||
assertIs<UserTrustedListEvent>(event)
|
||||
|
||||
assertTrue(event.isRetracted())
|
||||
assertEquals(emptyList<String>(), event.memberValues())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aCompleteListSerializesNoPartialMarker() {
|
||||
val content = TrustedListContent(members = listOf(TrustedListContentMember(pubkey = member, score = 99)))
|
||||
|
||||
val json = content.toContent()
|
||||
|
||||
assertFalse(json.contains("partial"), "encoded content was: $json")
|
||||
assertFalse(json.contains("total"), "encoded content was: $json")
|
||||
assertFalse(json.contains("\"id\""), "unused member keys must not be encoded: $json")
|
||||
assertEquals(content, TrustedListContent.parse(json))
|
||||
}
|
||||
}
|
||||
+340
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* 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.trustedLists
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.addressables.AddressableTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.addressables.tags.AddressMemberTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.events.EventTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.events.tags.EventMemberTag
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.externalIds.ExternalIdTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.tags.ListStatus
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.users.UserTrustedListEvent
|
||||
import com.vitorpamplona.quartz.experimental.trustedLists.users.tags.PubKeyMemberTag
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.utils.EventFactory
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class TrustedListEventTest {
|
||||
private val observer = "2efaa715bbb46dd5be6b7da8d7700266d11674b913b8178addb5c2e63d987331"
|
||||
private val tagEventId = "2f6a8652bde6fb5a974d6e06e4eae3b4f130140fd170b2686a291463f47a7451"
|
||||
private val tagAuthor = "e5272de914bd301755c439b88e6959a43c9d2664831f093c51e9c799a16a102f"
|
||||
private val dummySig = "00".repeat(64)
|
||||
|
||||
/** The pinned-tag pubkey list published by Tapestry's `refreshPinnedTags.runOnePin`. */
|
||||
private fun podcasterList(): Event =
|
||||
EventFactory.create<Event>(
|
||||
id = "2175c15e58ab5ac8ab05e06f3f56a6c44f7bc36b6a9de70bc125c48c21e1254f",
|
||||
pubKey = "a68dbf561cfe3da1b76f1e65c7d4d9cc116f79921b38a815fd75cb5460b4b599",
|
||||
createdAt = 1_787_253_028L,
|
||||
kind = UserTrustedListEvent.KIND,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("d", "tl-pin-2efaa715-e5272de9-podcaster"),
|
||||
arrayOf("title", "Podcaster"),
|
||||
arrayOf("metric", "pinned-tag-membership"),
|
||||
arrayOf("observer", observer),
|
||||
arrayOf("source-tag", tagEventId, tagAuthor, "podcaster"),
|
||||
arrayOf("cutoff", "1"),
|
||||
arrayOf("min-rank", "2"),
|
||||
arrayOf("p", "b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450", "", "99"),
|
||||
arrayOf("p", "ba2f394833658475e91680b898f9be0f1d850166c6a839dbe084d0266ad6e20a", "", "97"),
|
||||
arrayOf("p", "19fefd7f39c96d2ff76f87f7627ae79145bc971d8ab23205005939a5a913bc2f", "", "100"),
|
||||
),
|
||||
content =
|
||||
"{\"members\":[" +
|
||||
"{\"pubkey\":\"b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450\",\"endorsements\":4,\"disputes\":0,\"score\":99}," +
|
||||
"{\"pubkey\":\"ba2f394833658475e91680b898f9be0f1d850166c6a839dbe084d0266ad6e20a\",\"endorsements\":2,\"disputes\":0,\"score\":97}," +
|
||||
"{\"pubkey\":\"19fefd7f39c96d2ff76f87f7627ae79145bc971d8ab23205005939a5a913bc2f\",\"endorsements\":1,\"disputes\":0,\"score\":100}" +
|
||||
"]}",
|
||||
sig = dummySig,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun factoryBuildsEachKindInTheFamily() {
|
||||
assertTrue(EventFactory.isKnownKind(UserTrustedListEvent.KIND), "kind 30392 should be a known kind")
|
||||
assertTrue(EventFactory.isKnownKind(EventTrustedListEvent.KIND), "kind 30393 should be a known kind")
|
||||
assertTrue(EventFactory.isKnownKind(AddressableTrustedListEvent.KIND), "kind 30394 should be a known kind")
|
||||
assertTrue(EventFactory.isKnownKind(ExternalIdTrustedListEvent.KIND), "kind 30395 should be a known kind")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun kindsFollowThePlusTenRuleOverNip85() {
|
||||
assertEquals(30382 + 10, UserTrustedListEvent.KIND)
|
||||
assertEquals(30383 + 10, EventTrustedListEvent.KIND)
|
||||
assertEquals(30384 + 10, AddressableTrustedListEvent.KIND)
|
||||
assertEquals(30385 + 10, ExternalIdTrustedListEvent.KIND)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsesThePinnedTagPubKeyList() {
|
||||
val event = podcasterList()
|
||||
assertIs<UserTrustedListEvent>(event)
|
||||
|
||||
assertEquals("tl-pin-2efaa715-e5272de9-podcaster", event.listId())
|
||||
assertEquals("Podcaster", event.title())
|
||||
assertEquals("pinned-tag-membership", event.metric())
|
||||
assertEquals(observer, event.observer())
|
||||
assertEquals(1, event.cutoff())
|
||||
assertEquals(2, event.minRank())
|
||||
|
||||
val source = event.sourceTag()
|
||||
assertEquals(tagEventId, source?.eventId)
|
||||
assertEquals(tagAuthor, source?.author)
|
||||
assertEquals("podcaster", source?.slug)
|
||||
|
||||
assertEquals(3, event.memberCount())
|
||||
assertEquals(
|
||||
listOf(
|
||||
"b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450",
|
||||
"ba2f394833658475e91680b898f9be0f1d850166c6a839dbe084d0266ad6e20a",
|
||||
"19fefd7f39c96d2ff76f87f7627ae79145bc971d8ab23205005939a5a913bc2f",
|
||||
),
|
||||
event.memberValues(),
|
||||
)
|
||||
assertEquals(listOf(99, 97, 100), event.members().map { it.score })
|
||||
// the empty string at index 2 is a placeholder for the missing relay hint
|
||||
assertTrue(event.members().all { it.relayHint == null })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun theObserverIsProvenanceNotAMember() {
|
||||
val event = podcasterList()
|
||||
assertIs<UserTrustedListEvent>(event)
|
||||
|
||||
assertFalse(observer in event.memberValues(), "the observer must not be read back as a member")
|
||||
assertEquals(observer, event.observer())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readsTheContentEcho() {
|
||||
val event = podcasterList()
|
||||
assertIs<UserTrustedListEvent>(event)
|
||||
|
||||
val echo = event.contentEcho()
|
||||
assertEquals(3, echo?.members?.size)
|
||||
assertEquals("b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450", echo?.members?.first()?.memberValue())
|
||||
assertEquals(4, echo?.members?.first()?.endorsements)
|
||||
assertEquals(0, echo?.members?.first()?.disputes)
|
||||
assertEquals(99, echo?.members?.first()?.score)
|
||||
assertNull(echo?.partial, "a complete list carries no partial marker")
|
||||
assertEquals(event.memberValues(), echo?.members?.mapNotNull { it.memberValue() })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aMissingTruncatedTagMeansComplete() {
|
||||
val event = podcasterList()
|
||||
assertIs<UserTrustedListEvent>(event)
|
||||
|
||||
assertFalse(event.isTruncated())
|
||||
assertNull(event.truncatedTotal())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aTruncatedTagMeansNotExhaustive() {
|
||||
val event =
|
||||
EventFactory.create<Event>(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "a68dbf561cfe3da1b76f1e65c7d4d9cc116f79921b38a815fd75cb5460b4b599",
|
||||
createdAt = 1_787_253_028L,
|
||||
kind = EventTrustedListEvent.KIND,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("d", "tl-pin-notes-2efaa715-e5272de9-podcaster"),
|
||||
arrayOf("metric", "pinned-tag-notes"),
|
||||
arrayOf("truncated", "4211"),
|
||||
arrayOf("e", "f00dcafe00000000000000000000000000000000000000000000000000000000", "wss://nos.lol/", "88"),
|
||||
),
|
||||
content = "",
|
||||
sig = dummySig,
|
||||
)
|
||||
assertIs<EventTrustedListEvent>(event)
|
||||
|
||||
assertTrue(event.isTruncated())
|
||||
assertEquals(4211, event.truncatedTotal())
|
||||
assertNull(event.contentEcho(), "an empty content is not an echo")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aTruncatedTagWithoutATotalStillMeansNotExhaustive() {
|
||||
val event =
|
||||
EventFactory.create<Event>(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "a68dbf561cfe3da1b76f1e65c7d4d9cc116f79921b38a815fd75cb5460b4b599",
|
||||
createdAt = 1_787_253_028L,
|
||||
kind = EventTrustedListEvent.KIND,
|
||||
tags = arrayOf(arrayOf("d", "tl"), arrayOf("truncated")),
|
||||
content = "",
|
||||
sig = dummySig,
|
||||
)
|
||||
assertIs<EventTrustedListEvent>(event)
|
||||
|
||||
assertTrue(event.isTruncated(), "presence of the tag is what signals incompleteness")
|
||||
assertNull(event.truncatedTotal())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readsTheRetractionMarker() {
|
||||
val event =
|
||||
EventFactory.create<Event>(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "a68dbf561cfe3da1b76f1e65c7d4d9cc116f79921b38a815fd75cb5460b4b599",
|
||||
createdAt = 1_787_253_028L,
|
||||
kind = AddressableTrustedListEvent.KIND,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("d", "tag-applicability-nostr-event"),
|
||||
arrayOf("status", "retracted"),
|
||||
),
|
||||
content = "",
|
||||
sig = dummySig,
|
||||
)
|
||||
assertIs<AddressableTrustedListEvent>(event)
|
||||
|
||||
assertEquals(ListStatus.RETRACTED, event.status())
|
||||
assertTrue(event.isRetracted())
|
||||
assertEquals(emptyList<String>(), event.memberValues())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun discoveryTagsOnTheNoteListAreNotMembers() {
|
||||
val coordinate = "39999:$tagAuthor:podcaster"
|
||||
val noteId = "f00dcafe00000000000000000000000000000000000000000000000000000000"
|
||||
val event =
|
||||
EventFactory.create<Event>(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "a68dbf561cfe3da1b76f1e65c7d4d9cc116f79921b38a815fd75cb5460b4b599",
|
||||
createdAt = 1_787_253_028L,
|
||||
kind = EventTrustedListEvent.KIND,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("d", "tl-pin-notes-2efaa715-e5272de9-podcaster"),
|
||||
arrayOf("metric", "pinned-tag-notes"),
|
||||
arrayOf("observer", observer),
|
||||
arrayOf("a", coordinate),
|
||||
arrayOf("p", observer),
|
||||
arrayOf("e", noteId, "", "88"),
|
||||
),
|
||||
content = "",
|
||||
sig = dummySig,
|
||||
)
|
||||
assertIs<EventTrustedListEvent>(event)
|
||||
|
||||
assertEquals(listOf(noteId), event.memberValues())
|
||||
assertEquals(listOf(coordinate), event.aboutAddresses().map { it.toTag() })
|
||||
assertEquals(listOf(observer), event.aboutPubKeys().map { it.pubKey })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addressableMembersAreACoordinates() {
|
||||
val coordinate = "39999:$tagAuthor:podcaster"
|
||||
val event =
|
||||
EventFactory.create<Event>(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "a68dbf561cfe3da1b76f1e65c7d4d9cc116f79921b38a815fd75cb5460b4b599",
|
||||
createdAt = 1_787_253_028L,
|
||||
kind = AddressableTrustedListEvent.KIND,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("d", "tag-applicability-nostr-event"),
|
||||
arrayOf("metric", "tag-applicability"),
|
||||
arrayOf("a", coordinate),
|
||||
),
|
||||
content = "",
|
||||
sig = dummySig,
|
||||
)
|
||||
assertIs<AddressableTrustedListEvent>(event)
|
||||
|
||||
assertEquals(listOf(coordinate), event.memberValues())
|
||||
assertEquals(
|
||||
39999,
|
||||
event
|
||||
.members()
|
||||
.first()
|
||||
.toAddress()
|
||||
?.kind,
|
||||
)
|
||||
assertEquals(
|
||||
tagAuthor,
|
||||
event
|
||||
.members()
|
||||
.first()
|
||||
.toAddress()
|
||||
?.pubKeyHex,
|
||||
)
|
||||
assertEquals(
|
||||
"podcaster",
|
||||
event
|
||||
.members()
|
||||
.first()
|
||||
.toAddress()
|
||||
?.dTag,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun externalIdMembersKeepTheirNip73Hint() {
|
||||
val event =
|
||||
EventFactory.create<Event>(
|
||||
id = "00".repeat(32),
|
||||
pubKey = "a68dbf561cfe3da1b76f1e65c7d4d9cc116f79921b38a815fd75cb5460b4b599",
|
||||
createdAt = 1_787_253_028L,
|
||||
kind = ExternalIdTrustedListEvent.KIND,
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("d", "tl-pin-ext-podcaster"),
|
||||
arrayOf("i", "podcast:guid:c90e609a-df1e-596a-bd5e-57bcc8aad6cc", "https://fountain.fm/show/abc", "91"),
|
||||
),
|
||||
content = "",
|
||||
sig = dummySig,
|
||||
)
|
||||
assertIs<ExternalIdTrustedListEvent>(event)
|
||||
|
||||
val member = event.members().first()
|
||||
assertEquals("podcast:guid:c90e609a-df1e-596a-bd5e-57bcc8aad6cc", member.memberValue)
|
||||
assertEquals("https://fountain.fm/show/abc", member.hint)
|
||||
assertEquals(91, member.score)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun memberTagsRoundTripThroughTheirWireShape() {
|
||||
assertEquals(
|
||||
listOf("p", "b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450", "", "99"),
|
||||
PubKeyMemberTag("b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450", score = 99).toTagArray().toList(),
|
||||
)
|
||||
assertEquals(
|
||||
listOf("p", "b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450"),
|
||||
PubKeyMemberTag("b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450").toTagArray().toList(),
|
||||
)
|
||||
assertEquals(
|
||||
listOf("e", "f00dcafe00000000000000000000000000000000000000000000000000000000", "", "88"),
|
||||
EventMemberTag("f00dcafe00000000000000000000000000000000000000000000000000000000", score = 88).toTagArray().toList(),
|
||||
)
|
||||
assertEquals(
|
||||
listOf("a", "39999:$tagAuthor:podcaster"),
|
||||
AddressMemberTag("39999:$tagAuthor:podcaster").toTagArray().toList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user