This commit is contained in:
@@ -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 =>
|
||||
({
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user