Files
welshman/packages/client/src/relayLists.ts
T
2026-06-16 16:34:43 -07:00

115 lines
4.4 KiB
TypeScript

import {reject, nth, nthNe, nthEq, removeUndefined} from "@welshman/lib"
import {
RELAYS,
RelayMode,
asDecryptedEvent,
readList,
getRelaysFromList,
getRelayTags,
getListTags,
getRelayTagValues,
makeList,
makeEvent,
} from "@welshman/util"
import type {TrustedEvent, PublishedList} from "@welshman/util"
import {RepositoryCollection} from "./repositoryCollection.js"
import {Router} from "./router.js"
import {Network} from "./network.js"
import {User} from "./user.js"
import {Thunks} from "./thunk.js"
import type {IClient} from "./client.js"
/**
* NIP-65 relay lists, keyed by pubkey. This is the routing substrate every other
* outbox-model load depends on (see `Network.loadUsingOutbox`).
*/
export class RelayLists extends RepositoryCollection<PublishedList> {
constructor(ctx: IClient) {
super(ctx, {
filters: [{kinds: [RELAYS]}],
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
getKey: (list: PublishedList) => list.event.pubkey,
})
}
fetch(pubkey: string, relayHints: string[] = []) {
const filters = [{kinds: [RELAYS], authors: [pubkey], limit: 1}]
const networking = this.ctx.use(Network)
const router = this.ctx.use(Router)
return Promise.all([
networking.load({filters, relays: router.FromRelays(relayHints).getUrls()}),
networking.load({filters, relays: router.FromPubkey(pubkey).getUrls()}),
networking.load({filters, relays: router.Index().getUrls()}),
])
}
getRelaysForPubkey = (pubkey: string, mode?: RelayMode) =>
getRelaysFromList(this.get(pubkey), mode)
// NIP-65 relay-list mutations for the client's user
addRelay = async (url: string, mode: RelayMode) => {
const user = User.require(this.ctx)
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: RELAYS})
const dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
const tag = removeUndefined(["r", url, dup && dup[2] !== mode ? undefined : mode])
const tags = [...list.publicTags.filter(nthNe(1, url)), tag]
const event = {kind: list.kind, content: list.event?.content || "", tags}
return this.ctx.use(Thunks).publishToOutbox({event})
}
removeRelay = async (url: string, mode: RelayMode) => {
const user = User.require(this.ctx)
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: RELAYS})
const dup = getRelayTags(getListTags(list)).find(nthEq(1, url))
const alt = mode === RelayMode.Read ? RelayMode.Write : RelayMode.Read
const tags = list.publicTags.filter(nthNe(1, url))
// If we had a duplicate that was used as the alt mode, keep the alt
if (dup && (!dup[2] || dup[2] === alt)) {
tags.push(["r", url, alt])
}
const event = {kind: list.kind, content: list.event?.content || "", tags}
// Pass the old relay as an extra so it's notified of the removal too
return this.ctx.use(Thunks).publishToOutbox({event, relays: [url]})
}
setRelays = (tags: string[][]) => {
const router = this.ctx.use(Router)
const event = makeEvent(RELAYS, {tags})
const relays = router
.merge([router.Index(), router.FromRelays(getRelayTagValues(tags))])
.getUrls()
return this.ctx.use(Thunks).publish({event, relays})
}
setReadRelays = async (urls: string[]) => {
const user = User.require(this.ctx)
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: RELAYS})
const writeRelays = reject(nthEq(2, RelayMode.Read), getRelayTags(getListTags(list))).map(nth(1))
const writeTags = writeRelays.map(url => ["r", url, RelayMode.Write])
const readTags = urls.map(url => ["r", url, RelayMode.Read])
const tags = [...writeTags, ...readTags]
const event = {kind: list.kind, content: list.event?.content || "", tags}
return this.ctx.use(Thunks).publishToOutbox({event})
}
setWriteRelays = async (urls: string[]) => {
const user = User.require(this.ctx)
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: RELAYS})
const readRelays = reject(nthEq(2, RelayMode.Write), getRelayTags(getListTags(list))).map(nth(1))
const readTags = readRelays.map(url => ["r", url, RelayMode.Read])
const writeTags = urls.map(url => ["r", url, RelayMode.Write])
const tags = [...readTags, ...writeTags]
const event = {kind: list.kind, content: list.event?.content || "", tags}
return this.ctx.use(Thunks).publishToOutbox({event})
}
}