Split up commands and add them to domain modules
This commit is contained in:
@@ -1,7 +1,19 @@
|
||||
import {BLOCKED_RELAYS, asDecryptedEvent, readList, getRelaysFromList} from "@welshman/util"
|
||||
import {
|
||||
BLOCKED_RELAYS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
getRelaysFromList,
|
||||
makeList,
|
||||
makeEvent,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {User} from "./user.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
@@ -19,8 +31,30 @@ export class BlockedRelayLists extends RepositoryCollection<ReturnType<typeof re
|
||||
}
|
||||
|
||||
fetch(pubkey: string, relayHints: string[] = []) {
|
||||
return this.ctx.use(RelayLists).loadUsingOutbox(pubkey, {kinds: [BLOCKED_RELAYS]}, relayHints)
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [BLOCKED_RELAYS]}, relayHints)
|
||||
}
|
||||
|
||||
getBlockedRelays = (pubkey: string) => getRelaysFromList(this.get(pubkey))
|
||||
|
||||
addRelay = async (url: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: BLOCKED_RELAYS})
|
||||
const event = await addToListPublicly(list, ["relay", url]).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
removeRelay = async (url: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: BLOCKED_RELAYS})
|
||||
const event = await removeFromList(list, url).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
setRelays = (urls: string[]) =>
|
||||
this.ctx.use(Thunks).publish({
|
||||
event: makeEvent(BLOCKED_RELAYS, {tags: urls.map(url => ["relay", url])}),
|
||||
relays: this.ctx.use(Router).FromUser().getUrls(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {BLOSSOM_SERVERS, asDecryptedEvent, readList} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import {Network} from "./network.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
@@ -18,6 +18,6 @@ export class BlossomServerLists extends RepositoryCollection<ReturnType<typeof r
|
||||
}
|
||||
|
||||
fetch(pubkey: string, relayHints: string[] = []) {
|
||||
return this.ctx.use(RelayLists).loadUsingOutbox(pubkey, {kinds: [BLOSSOM_SERVERS]}, relayHints)
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [BLOSSOM_SERVERS]}, relayHints)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
import {uniq, reject, nth, now, nthNe, removeUndefined, nthEq} from "@welshman/lib"
|
||||
import {
|
||||
sendManagementRequest,
|
||||
addToListPublicly,
|
||||
addToListPrivately,
|
||||
updateList,
|
||||
removeFromList,
|
||||
makeHttpAuth,
|
||||
getListTags,
|
||||
getRelayTags,
|
||||
getRelayTagValues,
|
||||
getRelaysFromList,
|
||||
makeList,
|
||||
makeRoomCreateEvent,
|
||||
makeRoomDeleteEvent,
|
||||
makeRoomEditEvent,
|
||||
makeRoomJoinEvent,
|
||||
makeRoomLeaveEvent,
|
||||
makeRoomAddMemberEvent,
|
||||
makeRoomRemoveMemberEvent,
|
||||
isPublishedProfile,
|
||||
createProfile,
|
||||
editProfile,
|
||||
RelayMode,
|
||||
makeEvent,
|
||||
MESSAGING_RELAYS,
|
||||
BLOCKED_RELAYS,
|
||||
SEARCH_RELAYS,
|
||||
FOLLOWS,
|
||||
RELAYS,
|
||||
MUTES,
|
||||
PINS,
|
||||
prep,
|
||||
} from "@welshman/util"
|
||||
import type {ManagementRequest, EventTemplate, RoomMeta, Profile} from "@welshman/util"
|
||||
import {addMaximalFallbacks, Router} from "./router.js"
|
||||
import {MergedThunk, publishThunk} from "./thunk.js"
|
||||
import type {ThunkOptions} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {User} from "./user.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import {MessagingRelayLists} from "./messagingRelayLists.js"
|
||||
import {BlockedRelayLists} from "./blockedRelayLists.js"
|
||||
import {SearchRelayLists} from "./searchRelayLists.js"
|
||||
import {FollowLists} from "./follows.js"
|
||||
import {MuteLists} from "./mutes.js"
|
||||
import {PinLists} from "./pins.js"
|
||||
|
||||
export type SendWrappedOptions = Omit<
|
||||
ThunkOptions,
|
||||
"event" | "relays" | "recipient" | "client" | "user"
|
||||
> & {
|
||||
event: EventTemplate
|
||||
recipients: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The high-level "do an action" API: each method builds an event for the
|
||||
* client's user and publishes it via a thunk. Siblings (the user's lists, the
|
||||
* router) are resolved lazily through `ctx.use`; the acting user is `ctx.user`.
|
||||
*/
|
||||
export class Commands {
|
||||
constructor(readonly ctx: IClient) {}
|
||||
|
||||
private get user(): User {
|
||||
if (!this.ctx.user) {
|
||||
throw new Error("Commands require a signed-in user")
|
||||
}
|
||||
|
||||
return this.ctx.user
|
||||
}
|
||||
|
||||
private get router() {
|
||||
return this.ctx.use(Router)
|
||||
}
|
||||
|
||||
private get relayLists() {
|
||||
return this.ctx.use(RelayLists)
|
||||
}
|
||||
|
||||
private get messagingRelayLists() {
|
||||
return this.ctx.use(MessagingRelayLists)
|
||||
}
|
||||
|
||||
private get blockedRelayLists() {
|
||||
return this.ctx.use(BlockedRelayLists)
|
||||
}
|
||||
|
||||
private get searchRelayLists() {
|
||||
return this.ctx.use(SearchRelayLists)
|
||||
}
|
||||
|
||||
private get followLists() {
|
||||
return this.ctx.use(FollowLists)
|
||||
}
|
||||
|
||||
private get muteLists() {
|
||||
return this.ctx.use(MuteLists)
|
||||
}
|
||||
|
||||
private get pinLists() {
|
||||
return this.ctx.use(PinLists)
|
||||
}
|
||||
|
||||
private publish = (options: Omit<ThunkOptions, "client" | "user">) =>
|
||||
publishThunk({...options, client: this.ctx, user: this.user})
|
||||
|
||||
private fromUser = () => this.router.FromUser().policy(addMaximalFallbacks).getUrls()
|
||||
|
||||
private encryptToSelf = (payload: string) => this.user.nip44EncryptToSelf(payload)
|
||||
|
||||
// NIP 65
|
||||
|
||||
removeRelay = async (url: string, mode: RelayMode) => {
|
||||
const list = (await this.relayLists.forceLoad(this.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}
|
||||
const relays = this.fromUser()
|
||||
|
||||
// Make sure to notify the old relay too
|
||||
relays.push(url)
|
||||
|
||||
return this.publish({event, relays})
|
||||
}
|
||||
|
||||
addRelay = async (url: string, mode: RelayMode) => {
|
||||
const list = (await this.relayLists.forceLoad(this.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.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
setRelays = async (tags: string[][]) => {
|
||||
const event = makeEvent(RELAYS, {tags})
|
||||
const relays = this.router
|
||||
.merge([this.router.Index(), this.router.FromRelays(getRelayTagValues(tags))])
|
||||
.getUrls()
|
||||
|
||||
return this.publish({event, relays})
|
||||
}
|
||||
|
||||
setReadRelays = async (urls: string[]) => {
|
||||
const list = (await this.relayLists.forceLoad(this.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.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
setWriteRelays = async (urls: string[]) => {
|
||||
const list = (await this.relayLists.forceLoad(this.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.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
// NIP 17
|
||||
|
||||
removeMessagingRelay = async (url: string) => {
|
||||
const list =
|
||||
(await this.messagingRelayLists.forceLoad(this.user.pubkey, [])) ||
|
||||
makeList({kind: MESSAGING_RELAYS})
|
||||
const event = await removeFromList(list, url).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
addMessagingRelay = async (url: string) => {
|
||||
const list =
|
||||
(await this.messagingRelayLists.forceLoad(this.user.pubkey, [])) ||
|
||||
makeList({kind: MESSAGING_RELAYS})
|
||||
const event = await addToListPublicly(list, ["relay", url]).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
setMessagingRelays = async (urls: string[]) => {
|
||||
const event = makeEvent(MESSAGING_RELAYS, {tags: urls.map(url => ["relay", url])})
|
||||
|
||||
return this.publish({event, relays: this.router.FromUser().getUrls()})
|
||||
}
|
||||
|
||||
// Blocked Relays
|
||||
|
||||
removeBlockedRelay = async (url: string) => {
|
||||
const list =
|
||||
(await this.blockedRelayLists.forceLoad(this.user.pubkey, [])) ||
|
||||
makeList({kind: BLOCKED_RELAYS})
|
||||
const event = await removeFromList(list, url).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
addBlockedRelay = async (url: string) => {
|
||||
const list =
|
||||
(await this.blockedRelayLists.forceLoad(this.user.pubkey, [])) ||
|
||||
makeList({kind: BLOCKED_RELAYS})
|
||||
const event = await addToListPublicly(list, ["relay", url]).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
setBlockedRelays = async (urls: string[]) => {
|
||||
const event = makeEvent(BLOCKED_RELAYS, {tags: urls.map(url => ["relay", url])})
|
||||
|
||||
return this.publish({event, relays: this.router.FromUser().getUrls()})
|
||||
}
|
||||
|
||||
// Search Relays
|
||||
|
||||
removeSearchRelay = async (url: string) => {
|
||||
const list =
|
||||
(await this.searchRelayLists.forceLoad(this.user.pubkey, [])) ||
|
||||
makeList({kind: SEARCH_RELAYS})
|
||||
const event = await removeFromList(list, url).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
addSearchRelay = async (url: string) => {
|
||||
const list =
|
||||
(await this.searchRelayLists.forceLoad(this.user.pubkey, [])) ||
|
||||
makeList({kind: SEARCH_RELAYS})
|
||||
const event = await addToListPublicly(list, ["relay", url]).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
setSearchRelays = async (urls: string[]) => {
|
||||
const event = makeEvent(SEARCH_RELAYS, {tags: urls.map(url => ["relay", url])})
|
||||
|
||||
return this.publish({event, relays: this.router.FromUser().getUrls()})
|
||||
}
|
||||
|
||||
// NIP 01
|
||||
|
||||
setProfile = (profile: Profile) => {
|
||||
const relays = this.router.merge([this.router.Index(), this.router.FromUser()]).getUrls()
|
||||
const event = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
|
||||
|
||||
return this.publish({event, relays})
|
||||
}
|
||||
|
||||
// NIP 02
|
||||
|
||||
unfollow = async (value: string) => {
|
||||
const list =
|
||||
(await this.followLists.forceLoad(this.user.pubkey, [])) || makeList({kind: FOLLOWS})
|
||||
const event = await removeFromList(list, value).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
follow = async (tag: string[]) => {
|
||||
const list =
|
||||
(await this.followLists.forceLoad(this.user.pubkey, [])) || makeList({kind: FOLLOWS})
|
||||
const event = await addToListPublicly(list, tag).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
unmute = async (value: string) => {
|
||||
const list = (await this.muteLists.forceLoad(this.user.pubkey, [])) || makeList({kind: MUTES})
|
||||
const event = await removeFromList(list, value).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
mutePublicly = async (tag: string[]) => {
|
||||
const list = (await this.muteLists.forceLoad(this.user.pubkey, [])) || makeList({kind: MUTES})
|
||||
const event = await addToListPublicly(list, tag).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
mutePrivately = async (tag: string[]) => {
|
||||
const list = (await this.muteLists.forceLoad(this.user.pubkey, [])) || makeList({kind: MUTES})
|
||||
const event = await addToListPrivately(list, tag).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
setMutes = async ({
|
||||
publicTags,
|
||||
privateTags,
|
||||
}: {
|
||||
publicTags?: string[][]
|
||||
privateTags?: string[][]
|
||||
}) => {
|
||||
const list = (await this.muteLists.forceLoad(this.user.pubkey, [])) || makeList({kind: MUTES})
|
||||
const event = await updateList(list, {publicTags, privateTags}).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
unpin = async (value: string) => {
|
||||
const list = (await this.pinLists.forceLoad(this.user.pubkey, [])) || makeList({kind: PINS})
|
||||
const event = await removeFromList(list, value).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
pin = async (tag: string[]) => {
|
||||
const list = (await this.pinLists.forceLoad(this.user.pubkey, [])) || makeList({kind: PINS})
|
||||
const event = await addToListPublicly(list, tag).reconcile(this.encryptToSelf)
|
||||
|
||||
return this.publish({event, relays: this.fromUser()})
|
||||
}
|
||||
|
||||
// NIP 59
|
||||
|
||||
sendWrapped = async ({event, recipients, ...options}: SendWrappedOptions) => {
|
||||
// Stabilize the event id across the different wraps
|
||||
const stableEvent = prep(event, this.user.pubkey, now())
|
||||
|
||||
return new MergedThunk(
|
||||
await Promise.all(
|
||||
uniq(recipients).map(async recipient => {
|
||||
const relays = getRelaysFromList(await this.messagingRelayLists.load(recipient))
|
||||
|
||||
return this.publish({event: stableEvent, relays, recipient, ...options})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// NIP 86
|
||||
|
||||
manageRelay = async (url: string, request: ManagementRequest) => {
|
||||
url = url.replace(/^ws/, "http")
|
||||
|
||||
const authTemplate = await makeHttpAuth(url, "POST", JSON.stringify(request))
|
||||
const authEvent = await this.user.sign(authTemplate)
|
||||
|
||||
return sendManagementRequest(url, request, authEvent)
|
||||
}
|
||||
|
||||
// NIP 29
|
||||
|
||||
createRoom = (url: string, room: RoomMeta) =>
|
||||
this.publish({event: makeRoomCreateEvent(room), relays: [url]})
|
||||
|
||||
deleteRoom = (url: string, room: RoomMeta) =>
|
||||
this.publish({event: makeRoomDeleteEvent(room), relays: [url]})
|
||||
|
||||
editRoom = (url: string, room: RoomMeta) =>
|
||||
this.publish({event: makeRoomEditEvent(room), relays: [url]})
|
||||
|
||||
joinRoom = (url: string, room: RoomMeta) =>
|
||||
this.publish({event: makeRoomJoinEvent(room), relays: [url]})
|
||||
|
||||
leaveRoom = (url: string, room: RoomMeta) =>
|
||||
this.publish({event: makeRoomLeaveEvent(room), relays: [url]})
|
||||
|
||||
addRoomMember = (url: string, room: RoomMeta, pubkey: string) =>
|
||||
this.publish({event: makeRoomAddMemberEvent(room, pubkey), relays: [url]})
|
||||
|
||||
removeRoomMember = (url: string, room: RoomMeta, pubkey: string) =>
|
||||
this.publish({event: makeRoomRemoveMemberEvent(room, pubkey), relays: [url]})
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {defaultClientPolicies} from "./policies.js"
|
||||
/**
|
||||
* Creates a batteries-included client: a `Client` wired with the default client
|
||||
* policies (event ingestion, relay-stats collection, gift-wrap unwrapping).
|
||||
* Reach data modules via `client.use(Profiles)`, `client.use(Commands)`, etc.
|
||||
* Reach data modules via `client.use(Profiles)`, `client.use(FollowLists)`, etc.
|
||||
*
|
||||
* For a bare client (no default side effects) construct `new Client(...)`
|
||||
* directly, or pass your own `policies`.
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import {FOLLOWS, asDecryptedEvent, readList} from "@welshman/util"
|
||||
import {
|
||||
FOLLOWS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
makeList,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import {User} from "./user.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
@@ -18,6 +27,22 @@ export class FollowLists extends RepositoryCollection<ReturnType<typeof readList
|
||||
}
|
||||
|
||||
fetch(pubkey: string, relayHints: string[] = []) {
|
||||
return this.ctx.use(RelayLists).loadUsingOutbox(pubkey, {kinds: [FOLLOWS]}, relayHints)
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [FOLLOWS]}, relayHints)
|
||||
}
|
||||
|
||||
follow = async (tag: string[]) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: FOLLOWS})
|
||||
const event = await addToListPublicly(list, tag).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
unfollow = async (value: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: FOLLOWS})
|
||||
const event = await removeFromList(list, value).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import {get, writable} from "svelte/store"
|
||||
import {TaskQueue} from "@welshman/lib"
|
||||
import {getPubkeyTagValues} from "@welshman/util"
|
||||
import type {TrustedEvent, SignedEvent} from "@welshman/util"
|
||||
import {TaskQueue, uniq, now} from "@welshman/lib"
|
||||
import {getPubkeyTagValues, getRelaysFromList, prep} from "@welshman/util"
|
||||
import type {TrustedEvent, SignedEvent, EventTemplate} from "@welshman/util"
|
||||
import {Nip59} from "@welshman/signer"
|
||||
import {MergedThunk, Thunks} from "./thunk.js"
|
||||
import type {ThunkOptions} from "./thunk.js"
|
||||
import {User} from "./user.js"
|
||||
import {MessagingRelayLists} from "./messagingRelayLists.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
export type SendWrappedOptions = Omit<
|
||||
ThunkOptions,
|
||||
"event" | "relays" | "recipient" | "client" | "user"
|
||||
> & {
|
||||
event: EventTemplate
|
||||
recipients: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-client gift-wrap (NIP-59) state: the unwrap queue plus failure/dedup
|
||||
* tracking. Scoped to `ctx.user`, so a client only ever unwraps its own user's
|
||||
@@ -46,4 +58,23 @@ export class GiftWraps {
|
||||
|
||||
this.queue.push(wrap)
|
||||
}
|
||||
|
||||
// NIP-59: wrap an event for each recipient (using their messaging relays) and
|
||||
// publish the wraps as the client's user.
|
||||
publish = async ({event, recipients, ...options}: SendWrappedOptions) => {
|
||||
const user = User.require(this.ctx)
|
||||
|
||||
// Stabilize the event id across the different wraps
|
||||
const stableEvent = prep(event, user.pubkey, now())
|
||||
|
||||
return new MergedThunk(
|
||||
await Promise.all(
|
||||
uniq(recipients).map(async recipient => {
|
||||
const relays = getRelaysFromList(await this.ctx.use(MessagingRelayLists).load(recipient))
|
||||
|
||||
return this.ctx.use(Thunks).publish({event: stableEvent, relays, recipient, ...options})
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export * from "./client.js"
|
||||
export * from "./policies.js"
|
||||
export * from "./networking.js"
|
||||
export * from "./network.js"
|
||||
export * from "./stores.js"
|
||||
export * from "./clientData.js"
|
||||
export * from "./repositoryCollection.js"
|
||||
@@ -29,6 +29,7 @@ export * from "./feeds.js"
|
||||
export * from "./search.js"
|
||||
export * from "./sync.js"
|
||||
export * from "./giftWraps.js"
|
||||
export * from "./commands.js"
|
||||
export * from "./rooms.js"
|
||||
export * from "./relayManagement.js"
|
||||
export * from "./thunk.js"
|
||||
export * from "./createApp.js"
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import {MESSAGING_RELAYS, asDecryptedEvent, readList} from "@welshman/util"
|
||||
import {
|
||||
MESSAGING_RELAYS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
makeList,
|
||||
makeEvent,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {User} from "./user.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
@@ -19,6 +30,28 @@ export class MessagingRelayLists extends RepositoryCollection<ReturnType<typeof
|
||||
}
|
||||
|
||||
fetch(pubkey: string, relayHints: string[] = []) {
|
||||
return this.ctx.use(RelayLists).loadUsingOutbox(pubkey, {kinds: [MESSAGING_RELAYS]}, relayHints)
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [MESSAGING_RELAYS]}, relayHints)
|
||||
}
|
||||
|
||||
addRelay = async (url: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MESSAGING_RELAYS})
|
||||
const event = await addToListPublicly(list, ["relay", url]).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
removeRelay = async (url: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MESSAGING_RELAYS})
|
||||
const event = await removeFromList(list, url).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
setRelays = (urls: string[]) =>
|
||||
this.ctx.use(Thunks).publish({
|
||||
event: makeEvent(MESSAGING_RELAYS, {tags: urls.map(url => ["relay", url])}),
|
||||
relays: this.ctx.use(Router).FromUser().getUrls(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import {MUTES, asDecryptedEvent, readList} from "@welshman/util"
|
||||
import {
|
||||
MUTES,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
makeList,
|
||||
addToListPublicly,
|
||||
addToListPrivately,
|
||||
removeFromList,
|
||||
updateList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, PublishedList} from "@welshman/util"
|
||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import {Plaintext} from "./plaintext.js"
|
||||
import {User} from "./user.js"
|
||||
|
||||
/**
|
||||
* Kind-10000 mute lists, keyed by pubkey. Mute lists carry private entries in
|
||||
@@ -33,6 +44,38 @@ export class MuteLists extends RepositoryCollection<PublishedList> {
|
||||
}
|
||||
|
||||
fetch(pubkey: string, relayHints: string[] = []) {
|
||||
return this.ctx.use(RelayLists).loadUsingOutbox(pubkey, {kinds: [MUTES]}, relayHints)
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [MUTES]}, relayHints)
|
||||
}
|
||||
|
||||
mutePublicly = async (tag: string[]) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MUTES})
|
||||
const event = await addToListPublicly(list, tag).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
mutePrivately = async (tag: string[]) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MUTES})
|
||||
const event = await addToListPrivately(list, tag).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
unmute = async (value: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MUTES})
|
||||
const event = await removeFromList(list, value).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
setMutes = async (updates: {publicTags?: string[][]; privateTags?: string[][]}) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MUTES})
|
||||
const event = await updateList(list, updates).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import {chunk, first} from "@welshman/lib"
|
||||
import {RelayMode, getRelaysFromList, sortEventsDesc} from "@welshman/util"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {request, publish, diff, pull, push, makeLoader} from "@welshman/net"
|
||||
import type {
|
||||
Loader,
|
||||
@@ -8,19 +11,24 @@ import type {
|
||||
PullOptions,
|
||||
PushOptions,
|
||||
} from "@welshman/net"
|
||||
import {Router, addMinimalFallbacks} from "./router.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
* Net utilities bound to the client's net context (its pool + repository). Reach
|
||||
* it via `client.use(Networking)`; `load` is a shared, batched loader.
|
||||
* it via `client.use(Network)`; `load` is a shared, batched loader.
|
||||
*/
|
||||
export class Networking {
|
||||
export class Network {
|
||||
load: Loader
|
||||
|
||||
constructor(readonly ctx: IClient) {
|
||||
this.load = this.makeLoader({delay: 200, timeout: 3000, threshold: 0.5})
|
||||
this.load = this.makeLoader({delay: 50, timeout: 3000, threshold: 0.5})
|
||||
}
|
||||
|
||||
makeLoader = (options: Omit<LoaderOptions, "context">): Loader =>
|
||||
makeLoader({...options, context: this.ctx.netContext})
|
||||
|
||||
request = (options: Omit<RequestOptions, "context">) =>
|
||||
request({...options, context: this.ctx.netContext})
|
||||
|
||||
@@ -33,6 +41,22 @@ export class Networking {
|
||||
|
||||
push = (options: Omit<PushOptions, "context">) => push({...options, context: this.ctx.netContext})
|
||||
|
||||
makeLoader = (options: Omit<LoaderOptions, "context">): Loader =>
|
||||
makeLoader({...options, context: this.ctx.netContext})
|
||||
loadUsingOutbox = async (pubkey: string, filter: Filter = {}, relayHints: string[] = []) => {
|
||||
const filters: Filter[] = [{...filter, authors: [pubkey]}]
|
||||
const writeRelays = getRelaysFromList(await this.ctx.use(RelayLists).load(pubkey), RelayMode.Write)
|
||||
const allRelays = this.ctx
|
||||
.use(Router)
|
||||
.FromRelays([...relayHints, ...writeRelays])
|
||||
.policy(addMinimalFallbacks)
|
||||
.limit(8)
|
||||
.getUrls()
|
||||
|
||||
for (const relays of chunk(2, allRelays)) {
|
||||
const events = await this.load({filters, relays})
|
||||
|
||||
if (events.length > 0) {
|
||||
return first(sortEventsDesc(events))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,16 @@
|
||||
import {PINS, asDecryptedEvent, readList} from "@welshman/util"
|
||||
import {
|
||||
PINS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
makeList,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import {User} from "./user.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
@@ -18,6 +27,22 @@ export class PinLists extends RepositoryCollection<ReturnType<typeof readList>>
|
||||
}
|
||||
|
||||
fetch(pubkey: string, relayHints: string[] = []) {
|
||||
return this.ctx.use(RelayLists).loadUsingOutbox(pubkey, {kinds: [PINS]}, relayHints)
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [PINS]}, relayHints)
|
||||
}
|
||||
|
||||
pin = async (tag: string[]) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: PINS})
|
||||
const event = await addToListPublicly(list, tag).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
unpin = async (value: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: PINS})
|
||||
const event = await removeFromList(list, value).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import {derived, readable} from "svelte/store"
|
||||
import {readProfile, displayProfile, displayPubkey, PROFILE} from "@welshman/util"
|
||||
import {
|
||||
readProfile,
|
||||
displayProfile,
|
||||
displayPubkey,
|
||||
isPublishedProfile,
|
||||
createProfile,
|
||||
editProfile,
|
||||
PROFILE,
|
||||
} from "@welshman/util"
|
||||
import type {Profile} from "@welshman/util"
|
||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
@@ -18,7 +29,15 @@ export class Profiles extends RepositoryCollection<ReturnType<typeof readProfile
|
||||
}
|
||||
|
||||
fetch(pubkey: string, relayHints: string[] = []) {
|
||||
return this.ctx.use(RelayLists).loadUsingOutbox(pubkey, {kinds: [PROFILE]}, relayHints)
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [PROFILE]}, relayHints)
|
||||
}
|
||||
|
||||
publish = (profile: Profile) => {
|
||||
const router = this.ctx.use(Router)
|
||||
const relays = router.merge([router.Index(), router.FromUser()]).getUrls()
|
||||
const event = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
|
||||
|
||||
return this.ctx.use(Thunks).publish({event, relays})
|
||||
}
|
||||
|
||||
display = (pubkey: string | undefined) =>
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import {chunk, first} from "@welshman/lib"
|
||||
import {reject, nth, nthNe, nthEq, removeUndefined} from "@welshman/lib"
|
||||
import {
|
||||
RELAYS,
|
||||
RelayMode,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
getRelaysFromList,
|
||||
sortEventsDesc,
|
||||
getRelayTags,
|
||||
getListTags,
|
||||
getRelayTagValues,
|
||||
makeList,
|
||||
makeEvent,
|
||||
} from "@welshman/util"
|
||||
import type {Filter, TrustedEvent, PublishedList} from "@welshman/util"
|
||||
import type {TrustedEvent, PublishedList} from "@welshman/util"
|
||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||
import {Router, addMinimalFallbacks} from "./router.js"
|
||||
import {Networking} from "./networking.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, so it also exposes `loadUsingOutbox` for other
|
||||
* collections to use as their fetcher.
|
||||
* outbox-model load depends on (see `Network.loadUsingOutbox`).
|
||||
*/
|
||||
export class RelayLists extends RepositoryCollection<PublishedList> {
|
||||
constructor(ctx: IClient) {
|
||||
@@ -29,7 +34,7 @@ export class RelayLists extends RepositoryCollection<PublishedList> {
|
||||
|
||||
fetch(pubkey: string, relayHints: string[] = []) {
|
||||
const filters = [{kinds: [RELAYS], authors: [pubkey], limit: 1}]
|
||||
const networking = this.ctx.use(Networking)
|
||||
const networking = this.ctx.use(Network)
|
||||
const router = this.ctx.use(Router)
|
||||
|
||||
return Promise.all([
|
||||
@@ -42,27 +47,68 @@ export class RelayLists extends RepositoryCollection<PublishedList> {
|
||||
getRelaysForPubkey = (pubkey: string, mode?: RelayMode) =>
|
||||
getRelaysFromList(this.get(pubkey), mode)
|
||||
|
||||
// Load a pubkey's events using their advertised write relays (outbox model),
|
||||
// plus any explicit relay hints. This is the fetcher outbox-loaded collections
|
||||
// (profiles, follows, mutes, …) delegate to — it's a stable method, so calling
|
||||
// it doesn't build a fresh loader per call.
|
||||
// NIP-65 relay-list mutations for the client's user
|
||||
|
||||
loadUsingOutbox = async (pubkey: string, filter: Filter = {}, relayHints: string[] = []) => {
|
||||
const filters: Filter[] = [{...filter, authors: [pubkey]}]
|
||||
const writeRelays = getRelaysFromList(await this.load(pubkey), RelayMode.Write)
|
||||
const allRelays = this.ctx
|
||||
.use(Router)
|
||||
.FromRelays([...relayHints, ...writeRelays])
|
||||
.policy(addMinimalFallbacks)
|
||||
.limit(8)
|
||||
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()
|
||||
|
||||
for (const relays of chunk(2, allRelays)) {
|
||||
const events = await this.ctx.use(Networking).load({filters, relays})
|
||||
return this.ctx.use(Thunks).publish({event, relays})
|
||||
}
|
||||
|
||||
if (events.length > 0) {
|
||||
return first(sortEventsDesc(events))
|
||||
}
|
||||
}
|
||||
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})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import {makeHttpAuth, sendManagementRequest} from "@welshman/util"
|
||||
import type {ManagementRequest} from "@welshman/util"
|
||||
import {User} from "./user.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
* NIP-86 relay management. Signs an HTTP-auth event as the client's user and
|
||||
* sends an admin request to a relay's management endpoint.
|
||||
*/
|
||||
export class RelayManagement {
|
||||
constructor(readonly ctx: IClient) {}
|
||||
|
||||
post = async (url: string, request: ManagementRequest) => {
|
||||
url = url.replace(/^ws/, "http")
|
||||
|
||||
const authTemplate = await makeHttpAuth(url, "POST", JSON.stringify(request))
|
||||
const authEvent = await User.require(this.ctx).sign(authTemplate)
|
||||
|
||||
return sendManagementRequest(url, request, authEvent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
makeRoomCreateEvent,
|
||||
makeRoomDeleteEvent,
|
||||
makeRoomEditEvent,
|
||||
makeRoomJoinEvent,
|
||||
makeRoomLeaveEvent,
|
||||
makeRoomAddMemberEvent,
|
||||
makeRoomRemoveMemberEvent,
|
||||
} from "@welshman/util"
|
||||
import type {RoomMeta} from "@welshman/util"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {ThunkOptions} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
* NIP-29 relay-based group (room) management. Each method publishes the relevant
|
||||
* room event to the given relay as the client's user.
|
||||
*/
|
||||
export class Rooms {
|
||||
constructor(readonly ctx: IClient) {}
|
||||
|
||||
private publish = (url: string, event: ThunkOptions["event"]) =>
|
||||
this.ctx.use(Thunks).publish({event, relays: [url]})
|
||||
|
||||
create = (url: string, room: RoomMeta) => this.publish(url, makeRoomCreateEvent(room))
|
||||
|
||||
delete = (url: string, room: RoomMeta) => this.publish(url, makeRoomDeleteEvent(room))
|
||||
|
||||
edit = (url: string, room: RoomMeta) => this.publish(url, makeRoomEditEvent(room))
|
||||
|
||||
join = (url: string, room: RoomMeta) => this.publish(url, makeRoomJoinEvent(room))
|
||||
|
||||
leave = (url: string, room: RoomMeta) => this.publish(url, makeRoomLeaveEvent(room))
|
||||
|
||||
addMember = (url: string, room: RoomMeta, pubkey: string) =>
|
||||
this.publish(url, makeRoomAddMemberEvent(room, pubkey))
|
||||
|
||||
removeMember = (url: string, room: RoomMeta, pubkey: string) =>
|
||||
this.publish(url, makeRoomRemoveMemberEvent(room, pubkey))
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {PROFILE} from "@welshman/util"
|
||||
import type {PublishedProfile, RelayProfile} from "@welshman/util"
|
||||
import {throttled, deriveItems} from "@welshman/store"
|
||||
import type {IClient} from "./client.js"
|
||||
import {Networking} from "./networking.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {Profiles} from "./profiles.js"
|
||||
import {Topics} from "./topics.js"
|
||||
@@ -119,7 +119,7 @@ export class Searches {
|
||||
|
||||
searchProfiles = debounce(500, (search: string) => {
|
||||
if (search.length > 2) {
|
||||
this.ctx.use(Networking).load({
|
||||
this.ctx.use(Network).load({
|
||||
filters: [{kinds: [PROFILE], search}],
|
||||
relays: this.ctx.use(Router).Search().getUrls(),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import {SEARCH_RELAYS, asDecryptedEvent, readList} from "@welshman/util"
|
||||
import {
|
||||
SEARCH_RELAYS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
makeList,
|
||||
makeEvent,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {RepositoryCollection} from "./repositoryCollection.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {User} from "./user.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
@@ -19,6 +30,28 @@ export class SearchRelayLists extends RepositoryCollection<ReturnType<typeof rea
|
||||
}
|
||||
|
||||
fetch(pubkey: string, relayHints: string[] = []) {
|
||||
return this.ctx.use(RelayLists).loadUsingOutbox(pubkey, {kinds: [SEARCH_RELAYS]}, relayHints)
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [SEARCH_RELAYS]}, relayHints)
|
||||
}
|
||||
|
||||
addRelay = async (url: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: SEARCH_RELAYS})
|
||||
const event = await addToListPublicly(list, ["relay", url]).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
removeRelay = async (url: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: SEARCH_RELAYS})
|
||||
const event = await removeFromList(list, url).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.ctx.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
setRelays = (urls: string[]) =>
|
||||
this.ctx.use(Thunks).publish({
|
||||
event: makeEvent(SEARCH_RELAYS, {tags: urls.map(url => ["relay", url])}),
|
||||
relays: this.ctx.use(Router).FromUser().getUrls(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {isSignedEvent} from "@welshman/util"
|
||||
import type {Filter, SignedEvent} from "@welshman/util"
|
||||
import type {IClient} from "./client.js"
|
||||
import {Networking} from "./networking.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Relays} from "./relays.js"
|
||||
|
||||
export type AppSyncOpts = {
|
||||
@@ -32,7 +32,7 @@ export class Sync {
|
||||
}
|
||||
|
||||
pull = async ({relays, filters}: AppSyncOpts) => {
|
||||
const net = this.ctx.use(Networking)
|
||||
const net = this.ctx.use(Network)
|
||||
const events = this.query(filters).filter(isSignedEvent)
|
||||
|
||||
await Promise.all(
|
||||
@@ -45,7 +45,7 @@ export class Sync {
|
||||
}
|
||||
|
||||
push = async ({relays, filters}: AppSyncOpts) => {
|
||||
const net = this.ctx.use(Networking)
|
||||
const net = this.ctx.use(Network)
|
||||
const events = this.query(filters).filter(isSignedEvent)
|
||||
|
||||
await Promise.all(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {Subscriber} from "svelte/store"
|
||||
import {writable} from "svelte/store"
|
||||
import type {Override} from "@welshman/lib"
|
||||
import {append, TaskQueue, ensurePlural, remove, defer, sleep, nth, without} from "@welshman/lib"
|
||||
import {append, TaskQueue, ensurePlural, remove, defer, sleep, nth, uniq, without} from "@welshman/lib"
|
||||
import {
|
||||
HashedEvent,
|
||||
EventTemplate,
|
||||
@@ -14,8 +14,9 @@ import {
|
||||
import {PublishStatus, PublishResult, PublishOptions, PublishResultsByRelay} from "@welshman/net"
|
||||
import {Nip01Signer, Nip59} from "@welshman/signer"
|
||||
import type {IClient} from "./client.js"
|
||||
import {Networking} from "./networking.js"
|
||||
import type {User} from "./user.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router, addMaximalFallbacks} from "./router.js"
|
||||
import {User} from "./user.js"
|
||||
|
||||
export type ThunkOptions = Override<
|
||||
PublishOptions,
|
||||
@@ -112,7 +113,7 @@ export class Thunk {
|
||||
}
|
||||
|
||||
// Send it off
|
||||
await this.options.client.use(Networking).publish({
|
||||
await this.options.client.use(Network).publish({
|
||||
...this.options,
|
||||
event,
|
||||
onSuccess: (result: PublishResult) => {
|
||||
@@ -205,27 +206,6 @@ export class Thunk {
|
||||
}
|
||||
}
|
||||
|
||||
enqueue() {
|
||||
thunkQueue.push(this)
|
||||
|
||||
for (const url of this.options.relays) {
|
||||
this.options.client.tracker.track(this.event.id, url)
|
||||
}
|
||||
|
||||
this.options.client.repository.publish(this.event)
|
||||
thunks.update($thunks => append(this, $thunks))
|
||||
|
||||
this.controller.signal.addEventListener("abort", () => {
|
||||
if (this.wrap) {
|
||||
this.options.client.wrapManager.remove(this.wrap.id)
|
||||
} else {
|
||||
this.options.client.repository.removeEvent(this.event.id)
|
||||
}
|
||||
|
||||
thunks.update($thunks => remove(this, $thunks))
|
||||
})
|
||||
}
|
||||
|
||||
subscribe(subscriber: Subscriber<Thunk>) {
|
||||
this._subs.push(subscriber)
|
||||
|
||||
@@ -362,18 +342,6 @@ export const waitForThunkCompletion = (thunk: Thunk) =>
|
||||
})
|
||||
})
|
||||
|
||||
// Thunk state
|
||||
|
||||
export const thunks = writable<Thunk[]>([])
|
||||
|
||||
export const thunkQueue = new TaskQueue<Thunk>({
|
||||
batchSize: 10,
|
||||
batchDelay: 100,
|
||||
processItem: (thunk: Thunk) => {
|
||||
thunk.publish()
|
||||
},
|
||||
})
|
||||
|
||||
// Other thunk utilities
|
||||
|
||||
export const mergeThunks = (thunks: AbstractThunk[]) =>
|
||||
@@ -389,21 +357,76 @@ export function* flattenThunks(thunks: AbstractThunk[]): Iterable<Thunk> {
|
||||
}
|
||||
}
|
||||
|
||||
export const publishThunk = (options: ThunkOptions) => {
|
||||
const thunk = new Thunk(options)
|
||||
|
||||
thunk.enqueue()
|
||||
|
||||
return thunk
|
||||
}
|
||||
|
||||
export const abortThunk = (thunk: AbstractThunk) => {
|
||||
for (const child of flattenThunks([thunk])) {
|
||||
child.controller.abort()
|
||||
}
|
||||
}
|
||||
|
||||
export const retryThunk = (thunk: AbstractThunk) =>
|
||||
isMergedThunk(thunk)
|
||||
? mergeThunks(thunk.thunks.map(t => publishThunk(t.options)))
|
||||
: publishThunk(thunk.options)
|
||||
/**
|
||||
* Per-client thunk manager — the publish-side counterpart of `Network`. Owns
|
||||
* the client's optimistic-publish `history` store and the `queue` that paces
|
||||
* publishing. Reach it via `client.use(Thunks)`; `publish` fills in the client
|
||||
* and user, enqueues the thunk (optimistically writing it to the repository),
|
||||
* and returns it.
|
||||
*/
|
||||
export class Thunks {
|
||||
history = writable<Thunk[]>([])
|
||||
|
||||
queue = new TaskQueue<Thunk>({
|
||||
batchSize: 10,
|
||||
batchDelay: 100,
|
||||
processItem: (thunk: Thunk) => {
|
||||
thunk.publish()
|
||||
},
|
||||
})
|
||||
|
||||
constructor(readonly ctx: IClient) {}
|
||||
|
||||
publish = (options: Omit<ThunkOptions, "client" | "user">) => {
|
||||
const thunk = new Thunk({...options, client: this.ctx, user: User.require(this.ctx)})
|
||||
|
||||
this.enqueue(thunk)
|
||||
|
||||
return thunk
|
||||
}
|
||||
|
||||
// Publish as the user to their outbox (write) relays, plus any extra `relays`.
|
||||
publishToOutbox = ({
|
||||
relays = [],
|
||||
...options
|
||||
}: Omit<ThunkOptions, "client" | "user" | "relays"> & {relays?: string[]}) =>
|
||||
this.publish({
|
||||
...options,
|
||||
relays: uniq([
|
||||
...relays,
|
||||
...this.ctx.use(Router).FromUser().policy(addMaximalFallbacks).getUrls(),
|
||||
]),
|
||||
})
|
||||
|
||||
retry = (thunk: AbstractThunk) =>
|
||||
isMergedThunk(thunk)
|
||||
? mergeThunks(thunk.thunks.map(t => this.publish(t.options)))
|
||||
: this.publish(thunk.options)
|
||||
|
||||
private enqueue(thunk: Thunk) {
|
||||
this.queue.push(thunk)
|
||||
|
||||
for (const url of thunk.options.relays) {
|
||||
this.ctx.tracker.track(thunk.event.id, url)
|
||||
}
|
||||
|
||||
this.ctx.repository.publish(thunk.event)
|
||||
this.history.update($history => append(thunk, $history))
|
||||
|
||||
thunk.controller.signal.addEventListener("abort", () => {
|
||||
if (thunk.wrap) {
|
||||
this.ctx.wrapManager.remove(thunk.wrap.id)
|
||||
} else {
|
||||
this.ctx.repository.removeEvent(thunk.event.id)
|
||||
}
|
||||
|
||||
this.history.update($history => remove(thunk, $history))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {ISigner} from "@welshman/signer"
|
||||
import {LoggingSigner} from "./logging.js"
|
||||
import {getSignerFromSession} from "./session.js"
|
||||
import type {Session} from "./session.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
/**
|
||||
* A single identity: a pubkey plus the signer that proves it. A `Client` is
|
||||
@@ -38,6 +39,18 @@ export class User {
|
||||
return signer ? User.fromSigner(signer) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the client's signed-in user, throwing if there isn't one — the entry
|
||||
* point for actions that can only run as a user (publishing, signing).
|
||||
*/
|
||||
static require(ctx: IClient): User {
|
||||
if (!ctx.user) {
|
||||
throw new Error("This action requires a signed-in user")
|
||||
}
|
||||
|
||||
return ctx.user
|
||||
}
|
||||
|
||||
sign = (event: StampedEvent) => this.signer.sign(event)
|
||||
|
||||
nip44EncryptToSelf = (payload: string) => this.signer.nip44.encrypt(this.pubkey, payload)
|
||||
|
||||
Reference in New Issue
Block a user