Fix NIP conformance in domain kinds; add domain docs/skill
tests / tests (push) Failing after 5m15s

This commit is contained in:
2026-06-20 14:55:21 +00:00
committed by Jon Staab
parent e2a6ef21cd
commit ed17dcc412
33 changed files with 1406 additions and 658 deletions
-1
View File
@@ -84,7 +84,6 @@ export class Searches {
keys: [
"nip05",
{name: "name", weight: 0.8},
{name: "display_name", weight: 0.5},
{name: "about", weight: 0.3},
],
threshold: 0.3,
@@ -1,6 +1,7 @@
import {describe, it, expect} from "vitest"
import {makeSecret, BLOSSOM_SERVERS, NOTE, getTagValues, normalizeRelayUrl} from "@welshman/util"
import {makeSecret, BLOSSOM_SERVERS, NOTE, getTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {normalizeUrl} from "@welshman/lib"
import {Nip01Signer} from "@welshman/signer"
import {BlossomServerList, BlossomServerListBuilder} from "../src/kinds/BlossomServerList"
@@ -11,7 +12,7 @@ const s1 = "https://blossom.one.example/"
const s2 = "https://blossom.two.example/"
const s3 = "https://blossom.three.example/"
const norm = (url: string) => normalizeRelayUrl(url)
const norm = (url: string) => normalizeUrl(url)
const makeEvent = (o: Partial<TrustedEvent> = {}): TrustedEvent =>
({
+11 -9
View File
@@ -23,25 +23,27 @@ const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
}) as TrustedEvent
describe("RelayMembers", () => {
it("reads members from p tags", async () => {
const members = await RelayMembers.fromEvent(makeEvent({tags: [["p", a], ["p", b], ["p", a]]}))
it("reads members from member tags", async () => {
const members = await RelayMembers.fromEvent(
makeEvent({tags: [["member", a], ["member", b], ["member", a]]}),
)
expect(members.pubkeys().sort()).toEqual([a, b].sort())
expect(members.isMember(a)).toBe(true)
expect(members.isMember(c)).toBe(false)
})
it("round-trips with deduped p tags and passthrough", async () => {
it("round-trips with deduped member tags and passthrough", async () => {
const members = await RelayMembers.fromEvent(
makeEvent({tags: [["p", a], ["p", b], ["alt", "x"]]}),
makeEvent({tags: [["member", a], ["member", b], ["alt", "x"]]}),
)
const tmpl = await members.builder().toTemplate(signer)
expect(tmpl.kind).toBe(RELAY_MEMBERS)
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(2)
expect(tmpl.tags).toContainEqual(["p", a])
expect(tmpl.tags).toContainEqual(["p", b])
expect(tmpl.tags.filter(t => t[0] === "member").length).toBe(2)
expect(tmpl.tags).toContainEqual(["member", a])
expect(tmpl.tags).toContainEqual(["member", b])
expect(tmpl.tags).toContainEqual(["alt", "x"])
})
@@ -53,8 +55,8 @@ describe("RelayMembers", () => {
.removePubkey(b)
.toTemplate(signer)
expect(tmpl.tags.filter(t => t[0] === "p").length).toBe(1)
expect(tmpl.tags).toContainEqual(["p", a])
expect(tmpl.tags.filter(t => t[0] === "member").length).toBe(1)
expect(tmpl.tags).toContainEqual(["member", a])
})
it("throws on the wrong kind", async () => {
+18 -26
View File
@@ -9,34 +9,12 @@ export abstract class EventReader {
constructor(readonly event: TrustedEvent) {}
// Returns a reusable, class-bound reader factory over a fixed signer. Unlike a
// detached `fromEvent` (which would lose its binding, since it does
// `new this(event)`), this is invoked on the class up front, so it's safe
// point-free — e.g. `eventToItem: Profile.factory(signer)`. Pass the signer
// whenever you have one; the reader decides whether it needs it, so callers
// stay decoupled from which kinds carry encrypted content.
static factory<T extends EventReader>(this: new (event: TrustedEvent) => T, signer?: ISigner) {
const Reader = this
return async (event: TrustedEvent): Promise<T> => {
const reader = new Reader(event)
if (event.kind !== reader.kind) {
throw new Error(`Expected a kind ${reader.kind} event, got kind ${event.kind}`)
}
await reader.parse(signer)
return reader
}
}
static async fromEvent<T extends EventReader>(
this: new (event: TrustedEvent) => T,
private static async fromEventUsingSubclass<T extends EventReader>(
Reader: new (event: TrustedEvent) => T,
event: TrustedEvent,
signer?: ISigner,
): Promise<T> {
const reader = new this(event)
const reader = new Reader(event)
if (event.kind !== reader.kind) {
throw new Error(`Expected a kind ${reader.kind} event, got kind ${event.kind}`)
@@ -47,6 +25,20 @@ export abstract class EventReader {
return reader
}
static fromEvent<T extends EventReader>(
this: new (event: TrustedEvent) => T,
event: TrustedEvent,
signer?: ISigner,
): Promise<T> {
return EventReader.fromEventUsingSubclass(this, event, signer)
}
static factory<T extends EventReader>(this: new (event: TrustedEvent) => T, signer?: ISigner) {
const Reader = this
return (event: TrustedEvent) => EventReader.fromEventUsingSubclass(Reader, event, signer)
}
protected async parse(signer?: ISigner): Promise<void> {}
id() {
@@ -85,7 +77,7 @@ export abstract class EventReader {
return this.event.tags.some(spec(["-"]))
}
expires() {
expiration() {
const expiration = parseInt(getTagValue("expiration", this.event.tags) ?? "")
return isNaN(expiration) ? undefined : expiration
@@ -1,5 +1,5 @@
import {uniq, nthEq} from "@welshman/lib"
import {BLOSSOM_SERVERS, getTagValues, normalizeRelayUrl} from "@welshman/util"
import {uniq, nthEq, normalizeUrl} from "@welshman/lib"
import {BLOSSOM_SERVERS, getTagValues} from "@welshman/util"
import {ListReader} from "../ListReader.js"
import {ListBuilder} from "../ListBuilder.js"
@@ -8,11 +8,11 @@ export class BlossomServerList extends ListReader {
readonly kind = BLOSSOM_SERVERS
urls() {
return uniq(getTagValues("server", this.tags()).map(normalizeRelayUrl))
return uniq(getTagValues("server", this.tags()).map(url => normalizeUrl(url)))
}
includes(url: string) {
return this.urls().includes(normalizeRelayUrl(url))
return this.urls().includes(normalizeUrl(url))
}
builder() {
@@ -24,16 +24,16 @@ export class BlossomServerListBuilder extends ListBuilder<BlossomServerList> {
readonly kind = BLOSSOM_SERVERS
addUrl(url: string) {
return this.addPublic(["server", normalizeRelayUrl(url)])
return this.addPublic(["server", normalizeUrl(url)])
}
removeUrl(url: string) {
return this.drop(nthEq(1, normalizeRelayUrl(url)))
return this.drop(nthEq(1, normalizeUrl(url)))
}
setUrls(urls: string[]) {
this.clear()
return this.addPublic(...urls.map(url => ["server", normalizeRelayUrl(url)]))
return this.addPublic(...urls.map(url => ["server", normalizeUrl(url)]))
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
import {first} from "@welshman/lib"
import {COMMENT, Address, getAddress, getTagValue, isReplaceableKind} from "@welshman/util"
import {COMMENT, Address, getTagValue} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import type {ISigner} from "@welshman/signer"
import {EventReader} from "../EventReader.js"
@@ -68,7 +68,7 @@ export class CommentBuilder extends EventBuilder<Comment> {
this.rootTags = [["K", String(kind)], ["E", id], ["P", pubkey]]
if (identifier) {
this.rootTags.push(["A", id])
this.rootTags.push(["A", new Address(kind, pubkey, identifier).toString()])
}
return this
@@ -78,7 +78,7 @@ export class CommentBuilder extends EventBuilder<Comment> {
this.parentTags = [["k", String(kind)], ["e", id], ["p", pubkey]]
if (identifier) {
this.parentTags.push(["a", id])
this.parentTags.push(["a", new Address(kind, pubkey, identifier).toString()])
}
return this
@@ -1,4 +1,4 @@
import {nthEq, last} from "@welshman/lib"
import {nthNe, last} from "@welshman/lib"
import {HANDLER_RECOMMENDATION, getAddressTags, getAddressTagValues} from "@welshman/util"
import {EventReader} from "../EventReader.js"
import {EventBuilder} from "../EventBuilder.js"
@@ -47,7 +47,7 @@ export class HandlerRecommendationBuilder extends EventBuilder<HandlerRecommenda
}
removeRecommendation(address: string) {
this.addressTags = this.addressTags.filter(nthEq(1, address))
this.addressTags = this.addressTags.filter(nthNe(1, address))
return this
}
-14
View File
@@ -41,10 +41,6 @@ export class Profile extends EventReader {
return this.values.name
}
displayName(): Maybe<string> {
return this.values.display_name
}
nip05(): Maybe<string> {
return this.values.nip05
}
@@ -74,10 +70,6 @@ export class Profile extends EventReader {
if (name) return ellipsize(name, 60).trim()
const displayName= this.displayName()
if (displayName) return ellipsize(displayName, 60).trim()
return displayPubkey(this.event.pubkey).trim() || fallback.trim()
}
@@ -107,12 +99,6 @@ export class ProfileBuilder extends EventBuilder<Profile> {
return this
}
setDisplayName(displayName: string) {
this.values.displayName = displayName
return this
}
setNip05(nip05: string) {
this.values.nip05 = nip05
+9 -5
View File
@@ -1,14 +1,15 @@
import {uniq, nth, nthNe, uniqBy} from "@welshman/lib"
import {RELAY_MEMBERS, getPubkeyTagValues} from "@welshman/util"
import {RELAY_MEMBERS, getTagValues} from "@welshman/util"
import {EventReader} from "../EventReader.js"
import {EventBuilder} from "../EventBuilder.js"
// Flotilla kind-13534 relay/space member-list snapshot.
// Flotilla kind-13534 relay/space member-list snapshot. Members are carried in
// NIP-43 `member` tags, and the event is NIP-70 protected (`-`).
export class RelayMembers extends EventReader {
readonly kind = RELAY_MEMBERS
pubkeys() {
return uniq(getPubkeyTagValues(this.event.tags))
return uniq(getTagValues("member", this.event.tags))
}
isMember(pubkey: string) {
@@ -28,11 +29,14 @@ export class RelayMembersBuilder extends EventBuilder<RelayMembers> {
constructor(readonly reader?: RelayMembers) {
super(reader)
this.pubkeyTags = uniqBy(nth(1), this.consumeTags("p"))
this.pubkeyTags = uniqBy(nth(1), this.consumeTags("member"))
// NIP-43 requires kind-13534 member lists to be NIP-70 protected.
this.setProtected(true)
}
addPubkey(pubkey: string) {
this.pubkeyTags = uniqBy(nth(1), [...this.pubkeyTags, ["p", pubkey]])
this.pubkeyTags = uniqBy(nth(1), [...this.pubkeyTags, ["member", pubkey]])
return this
}
+22 -17
View File
@@ -7,7 +7,7 @@ import {EventBuilder} from "../EventBuilder.js"
export class Report extends EventReader {
readonly kind = REPORT
reportedPubkey() {
pubkey() {
return getTagValue("p", this.event.tags)
}
@@ -16,7 +16,7 @@ export class Report extends EventReader {
}
reason() {
return getTag("e", this.event.tags)?.[2]
return getTag("e", this.event.tags)?.[2] ?? getTag("p", this.event.tags)?.[2]
}
builder() {
@@ -27,29 +27,26 @@ export class Report extends EventReader {
export class ReportBuilder extends EventBuilder<Report> {
readonly kind = REPORT
reportedPubkey?: string
eventId?: string
pTag?: string[]
eTag?: string[]
reason?: string
constructor(readonly reader?: Report) {
super(reader)
const p = first(this.consumeTags("p"))
const e = first(this.consumeTags("e"))
this.reportedPubkey = p?.[1]
this.eventId = e?.[1]
this.reason = e?.[2]
this.pTag = first(this.consumeTags("p"))
this.eTag = first(this.consumeTags("e"))
this.reason = this.eTag?.[2] ?? this.pTag?.[2]
}
setReportedPubkey(reportedPubkey: string) {
this.reportedPubkey = reportedPubkey
setPubkey(pubkey: string) {
this.pTag = ["p", pubkey]
return this
}
setEventId(eventId: string) {
this.eventId = eventId
this.eTag = ["e", eventId]
return this
}
@@ -63,12 +60,20 @@ export class ReportBuilder extends EventBuilder<Report> {
protected buildTags() {
const tags: string[][] = []
if (this.reportedPubkey) {
tags.push(["p", this.reportedPubkey])
if (this.pTag) {
if (this.pTag.length === 2) {
this.pTag.push(this.reason)
}
tags.push(this.pTag)
}
if (this.eventId) {
tags.push(["e", this.eventId, ...(this.reason ? [this.reason] : [])])
if (this.eTag) {
if (this.eTag.length === 2) {
this.eTag.push(this.reason)
}
tags.push(this.eTag)
}
return tags