Compare commits
5 Commits
72ab746254
...
dfeb7a747b
| Author | SHA1 | Date | |
|---|---|---|---|
| dfeb7a747b | |||
| eb451d795b | |||
| 393c95e107 | |||
| 772895e3ab | |||
| fafa3b172e |
@@ -3,7 +3,7 @@ import {call} from "@welshman/lib"
|
||||
import {Pool, Tracker, Repository, WrapManager} from "@welshman/net"
|
||||
import type {NetContext, AdapterFactory} from "@welshman/net"
|
||||
import type {User} from "./user.js"
|
||||
import type {ClientPolicy} from "./policies.js"
|
||||
import type {ClientPolicy} from "./policy.js"
|
||||
|
||||
export type ClientConfig = {
|
||||
dufflepudUrl?: string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Client} from "./client.js"
|
||||
import type {ClientOptions} from "./client.js"
|
||||
import {defaultClientPolicies} from "./policies.js"
|
||||
import {defaultClientPolicies} from "./policy.js"
|
||||
|
||||
/**
|
||||
* Creates a batteries-included client: a `Client` wired with the default client
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
export * from "./client.js"
|
||||
export * from "./policies.js"
|
||||
export * from "./network.js"
|
||||
export * from "./stores.js"
|
||||
export * from "./clientData.js"
|
||||
export * from "./policy.js"
|
||||
export * from "./user.js"
|
||||
export * from "./router.js"
|
||||
export * from "./relays.js"
|
||||
export * from "./relayStats.js"
|
||||
export * from "./relayLists.js"
|
||||
export * from "./blockedRelayLists.js"
|
||||
export * from "./plaintext.js"
|
||||
export * from "./profiles.js"
|
||||
export * from "./follows.js"
|
||||
export * from "./mutes.js"
|
||||
export * from "./pins.js"
|
||||
export * from "./blossom.js"
|
||||
export * from "./messagingRelayLists.js"
|
||||
export * from "./searchRelayLists.js"
|
||||
export * from "./handles.js"
|
||||
export * from "./zappers.js"
|
||||
export * from "./topics.js"
|
||||
export * from "./tags.js"
|
||||
export * from "./session.js"
|
||||
export * from "./logging.js"
|
||||
export * from "./wot.js"
|
||||
export * from "./feeds.js"
|
||||
export * from "./search.js"
|
||||
export * from "./sync.js"
|
||||
export * from "./giftWraps.js"
|
||||
export * from "./rooms.js"
|
||||
export * from "./relayManagement.js"
|
||||
export * from "./thunk.js"
|
||||
export * from "./createApp.js"
|
||||
export * from "./plugins/base.js"
|
||||
export * from "./plugins/network.js"
|
||||
export * from "./plugins/stores.js"
|
||||
export * from "./plugins/router.js"
|
||||
export * from "./plugins/relays.js"
|
||||
export * from "./plugins/relayStats.js"
|
||||
export * from "./plugins/relayLists.js"
|
||||
export * from "./plugins/blockedRelayLists.js"
|
||||
export * from "./plugins/plaintext.js"
|
||||
export * from "./plugins/profiles.js"
|
||||
export * from "./plugins/follows.js"
|
||||
export * from "./plugins/mutes.js"
|
||||
export * from "./plugins/pins.js"
|
||||
export * from "./plugins/blossom.js"
|
||||
export * from "./plugins/messagingRelayLists.js"
|
||||
export * from "./plugins/searchRelayLists.js"
|
||||
export * from "./plugins/handles.js"
|
||||
export * from "./plugins/zappers.js"
|
||||
export * from "./plugins/topics.js"
|
||||
export * from "./plugins/tags.js"
|
||||
export * from "./plugins/wot.js"
|
||||
export * from "./plugins/feeds.js"
|
||||
export * from "./plugins/search.js"
|
||||
export * from "./plugins/sync.js"
|
||||
export * from "./plugins/wraps.js"
|
||||
export * from "./plugins/rooms.js"
|
||||
export * from "./plugins/relayManagement.js"
|
||||
export * from "./plugins/thunk.js"
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import {writable} from "svelte/store"
|
||||
import {writable, derived} from "svelte/store"
|
||||
import type {Readable, Unsubscriber} from "svelte/store"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {deriveItems, getter, makeDeriveItem, makeLoadItem, makeForceLoadItem} from "@welshman/store"
|
||||
import type {EventToItem, ItemsByKey, MakeLoadItemOptions} from "@welshman/store"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
import {Stores} from "./stores.js"
|
||||
|
||||
/**
|
||||
@@ -17,6 +17,13 @@ export type Projection<T> = {
|
||||
|
||||
export const projection = <T>($: Readable<T>, get = getter($)) => ({$, get})
|
||||
|
||||
/**
|
||||
* Build a `Projection` derived from another `Projection`: re-read `src`
|
||||
* reactively via `.$` or synchronously via `.get()`.
|
||||
*/
|
||||
export const projectFrom = <S, U>(src: Projection<S>, read: ($: S) => U): Projection<U> =>
|
||||
projection(derived(src.$, read), () => read(src.get()))
|
||||
|
||||
/**
|
||||
* Base class for a reactive, keyed collection of "local" (non-event) data —
|
||||
* things like relay stats or NIP-11 profiles that aren't backed by the
|
||||
@@ -26,7 +33,7 @@ export const projection = <T>($: Readable<T>, get = getter($)) => ({$, get})
|
||||
* snapshot via `.get()`. Per-key access is `one(key)`, a plain on-demand store
|
||||
* (snapshot with svelte's `get(...)`, or read `get(key)` directly).
|
||||
*/
|
||||
export class ClientData<T> {
|
||||
export class MapPlugin<T> {
|
||||
protected store = writable(new Map<string, T>())
|
||||
index: Projection<ItemsByKey<T>>
|
||||
all: Projection<T[]>
|
||||
@@ -39,12 +46,11 @@ export class ClientData<T> {
|
||||
this.one = makeDeriveItem(this.store)
|
||||
}
|
||||
|
||||
keys = () => this.index.get().keys()
|
||||
|
||||
values = () => this.index.get().values()
|
||||
|
||||
get = (key: string) => this.index.get().get(key)
|
||||
|
||||
project = <U>(key: string, read: (item: Maybe<T>) => U): Projection<U> =>
|
||||
projection(derived(this.one(key), read), () => read(this.get(key)))
|
||||
|
||||
set = (key: string, value: T) => {
|
||||
this.store.update($items => {
|
||||
$items.set(key, value)
|
||||
@@ -93,11 +99,11 @@ export class ClientData<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* A `ClientData` collection that knows how to lazily load items by key from the
|
||||
* A `MapPlugin` collection that knows how to lazily load items by key from the
|
||||
* network. Subclasses implement `fetch`; `load`/`forceLoad`/`one` are derived
|
||||
* from it (with per-key caching and backoff via `makeLoadItem`).
|
||||
*/
|
||||
export abstract class LoadableData<T> extends ClientData<T> {
|
||||
export abstract class LoadableMapPlugin<T> extends MapPlugin<T> {
|
||||
load: (key: string, ...args: any[]) => Promise<Maybe<T>>
|
||||
forceLoad: (key: string, ...args: any[]) => Promise<Maybe<T>>
|
||||
|
||||
@@ -118,7 +124,7 @@ export abstract class LoadableData<T> extends ClientData<T> {
|
||||
}
|
||||
}
|
||||
|
||||
export type DerivedDataOptions<T> = {
|
||||
export type DerivedPluginOptions<T> = {
|
||||
filters: Filter[]
|
||||
eventToItem: EventToItem<T>
|
||||
getKey: (item: T) => string
|
||||
@@ -135,7 +141,7 @@ export type DerivedDataOptions<T> = {
|
||||
* `index` (map) and `all` (values) are `Projection`s — subscribe via `.$`,
|
||||
* snapshot via `.get()`. Per-key access is `one(key)`, a plain on-demand store.
|
||||
*/
|
||||
export abstract class DerivedData<T> {
|
||||
export abstract class DerivedPlugin<T> {
|
||||
index: Projection<ItemsByKey<T>>
|
||||
all: Projection<T[]>
|
||||
one: (key?: string, ...args: any[]) => Readable<Maybe<T>>
|
||||
@@ -146,7 +152,7 @@ export abstract class DerivedData<T> {
|
||||
|
||||
constructor(
|
||||
protected readonly ctx: IClient,
|
||||
options: DerivedDataOptions<T>,
|
||||
options: DerivedPluginOptions<T>,
|
||||
) {
|
||||
const index = ctx.use(Stores).itemsByKey<T>({
|
||||
filters: options.filters,
|
||||
@@ -165,9 +171,8 @@ export abstract class DerivedData<T> {
|
||||
this.one = makeDeriveItem(index, this.load)
|
||||
}
|
||||
|
||||
keys = () => this.index.get().keys()
|
||||
|
||||
values = () => this.index.get().values()
|
||||
|
||||
get = (key: string) => this.index.get().get(key)
|
||||
|
||||
project = <U>(key: string, read: (item: Maybe<T>) => U): Projection<U> =>
|
||||
projection(derived(this.one(key), read), () => read(this.get(key)))
|
||||
}
|
||||
+7
-5
@@ -9,19 +9,20 @@ import {
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {DerivedData} from "./clientData.js"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {User} from "./user.js"
|
||||
import {User} from "../user.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* Kind-10006 blocked-relay lists, keyed by pubkey. Loaded via the outbox model,
|
||||
* so it depends on the relay-list collection. Feeds `RelayStats.getQuality` so
|
||||
* blocked relays are never selected.
|
||||
*/
|
||||
export class BlockedRelayLists extends DerivedData<ReturnType<typeof readList>> {
|
||||
export class BlockedRelayLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx, {
|
||||
filters: [{kinds: [BLOCKED_RELAYS]}],
|
||||
@@ -34,7 +35,8 @@ export class BlockedRelayLists extends DerivedData<ReturnType<typeof readList>>
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [BLOCKED_RELAYS]}, relayHints)
|
||||
}
|
||||
|
||||
getBlockedRelays = (pubkey: string) => getRelaysFromList(this.get(pubkey))
|
||||
urls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list))
|
||||
|
||||
addRelay = async (url: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
@@ -1,14 +1,14 @@
|
||||
import {BLOSSOM_SERVERS, asDecryptedEvent, readList} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {DerivedData} from "./clientData.js"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import {Network} from "./network.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* Blossom server lists (kind 10063), keyed by pubkey. Loaded via the outbox
|
||||
* model (the author's write relays), so it depends on the relay-list collection.
|
||||
*/
|
||||
export class BlossomServerLists extends DerivedData<ReturnType<typeof readList>> {
|
||||
export class BlossomServerLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx, {
|
||||
filters: [{kinds: [BLOSSOM_SERVERS]}],
|
||||
@@ -1,7 +1,7 @@
|
||||
import {Scope, FeedController} from "@welshman/feeds"
|
||||
import type {FeedControllerOptions, Feed} from "@welshman/feeds"
|
||||
import type {AdapterContext} from "@welshman/net"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
import {Router} from "./router.js"
|
||||
import {Wot} from "./wot.js"
|
||||
|
||||
@@ -7,17 +7,17 @@ import {
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {DerivedData} from "./clientData.js"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import {User} from "./user.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import {User} from "../user.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* Kind-3 follow lists, keyed by pubkey. Loaded via the outbox model (the
|
||||
* author's write relays), so it depends on the relay-list collection.
|
||||
*/
|
||||
export class FollowLists extends DerivedData<ReturnType<typeof readList>> {
|
||||
export class FollowLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx, {
|
||||
filters: [{kinds: [FOLLOWS]}],
|
||||
@@ -3,9 +3,9 @@ import type {Maybe} from "@welshman/lib"
|
||||
import {queryProfile, displayNip05} from "@welshman/util"
|
||||
import type {Handle} from "@welshman/util"
|
||||
import {deriveDeduplicated} from "@welshman/store"
|
||||
import {LoadableData, projection} from "./clientData.js"
|
||||
import type {Projection} from "./clientData.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import {LoadableMapPlugin, projection} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
import type {IClient} from "../client.js"
|
||||
import {Profiles} from "./profiles.js"
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,7 @@ import {Profiles} from "./profiles.js"
|
||||
* user privacy). Depends on the profiles collection to resolve a pubkey's
|
||||
* handle.
|
||||
*/
|
||||
export class Handles extends LoadableData<Handle> {
|
||||
export class Handles extends LoadableMapPlugin<Handle> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx)
|
||||
}
|
||||
+9
-4
@@ -2,25 +2,27 @@ import {
|
||||
MESSAGING_RELAYS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
getRelaysFromList,
|
||||
makeList,
|
||||
makeEvent,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {DerivedData} from "./clientData.js"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {User} from "./user.js"
|
||||
import {User} from "../user.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* Kind-10050 messaging relay lists (NIP-17), keyed by pubkey. Loaded via the
|
||||
* outbox model (the author's write relays), so it depends on the relay-list
|
||||
* collection.
|
||||
*/
|
||||
export class MessagingRelayLists extends DerivedData<ReturnType<typeof readList>> {
|
||||
export class MessagingRelayLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx, {
|
||||
filters: [{kinds: [MESSAGING_RELAYS]}],
|
||||
@@ -33,6 +35,9 @@ export class MessagingRelayLists extends DerivedData<ReturnType<typeof readList>
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [MESSAGING_RELAYS]}, relayHints)
|
||||
}
|
||||
|
||||
urls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list))
|
||||
|
||||
addRelay = async (url: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MESSAGING_RELAYS})
|
||||
@@ -9,18 +9,18 @@ import {
|
||||
updateList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, PublishedList} from "@welshman/util"
|
||||
import {DerivedData} from "./clientData.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import type {IClient} from "../client.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import {Plaintext} from "./plaintext.js"
|
||||
import {User} from "./user.js"
|
||||
import {User} from "../user.js"
|
||||
|
||||
/**
|
||||
* Kind-10000 mute lists, keyed by pubkey. Mute lists carry private entries in
|
||||
* encrypted content, so decoding goes through the plaintext cache.
|
||||
*/
|
||||
export class MuteLists extends DerivedData<PublishedList> {
|
||||
export class MuteLists extends DerivedPlugin<PublishedList> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx, {
|
||||
filters: [{kinds: [MUTES]}],
|
||||
@@ -11,9 +11,10 @@ import type {
|
||||
PullOptions,
|
||||
PushOptions,
|
||||
} from "@welshman/net"
|
||||
import {Router, addMinimalFallbacks} from "./router.js"
|
||||
import {addMinimalFallbacks} from "@welshman/router"
|
||||
import {Router} from "./router.js"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* Net utilities bound to the client's net context (its pool + repository). Reach
|
||||
@@ -7,17 +7,17 @@ import {
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {DerivedData} from "./clientData.js"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import {User} from "./user.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import {User} from "../user.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* NIP-51 pin lists (kind 10001), keyed by pubkey. Loaded via the outbox model
|
||||
* (the author's write relays), so it depends on the relay-list collection.
|
||||
*/
|
||||
export class PinLists extends DerivedData<ReturnType<typeof readList>> {
|
||||
export class PinLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx, {
|
||||
filters: [{kinds: [PINS]}],
|
||||
@@ -1,12 +1,12 @@
|
||||
import {decrypt} from "@welshman/signer"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {ClientData} from "./clientData.js"
|
||||
import {MapPlugin} from "./base.js"
|
||||
|
||||
/**
|
||||
* A cache of decrypted event content, keyed by event id.
|
||||
*/
|
||||
export class Plaintext extends ClientData<string> {
|
||||
export class Plaintext extends MapPlugin<string> {
|
||||
ensure = async (event: TrustedEvent): Promise<Maybe<string>> => {
|
||||
if (this.ctx.user?.pubkey !== event.pubkey) return
|
||||
|
||||
@@ -10,18 +10,18 @@ import {
|
||||
} from "@welshman/util"
|
||||
import type {Profile} from "@welshman/util"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {DerivedData, projection} from "./clientData.js"
|
||||
import type {Projection} from "./clientData.js"
|
||||
import {DerivedPlugin, projection} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* Kind-0 profiles, keyed by pubkey. Loaded via the outbox model (the author's
|
||||
* write relays), resolved through the relay-list collection at fetch time.
|
||||
*/
|
||||
export class Profiles extends DerivedData<ReturnType<typeof readProfile>> {
|
||||
export class Profiles extends DerivedPlugin<ReturnType<typeof readProfile>> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx, {
|
||||
filters: [{kinds: [PROFILE]}],
|
||||
@@ -12,18 +12,20 @@ import {
|
||||
makeEvent,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, PublishedList} from "@welshman/util"
|
||||
import {DerivedData} from "./clientData.js"
|
||||
import {Router, addMinimalFallbacks} from "./router.js"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
import {addMinimalFallbacks} from "@welshman/router"
|
||||
import {Router} from "./router.js"
|
||||
import {Network} from "./network.js"
|
||||
import {User} from "./user.js"
|
||||
import {User} from "../user.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {IClient} from "./client.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 DerivedData<PublishedList> {
|
||||
export class RelayLists extends DerivedPlugin<PublishedList> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx, {
|
||||
filters: [{kinds: [RELAYS]}],
|
||||
@@ -44,8 +46,14 @@ export class RelayLists extends DerivedData<PublishedList> {
|
||||
])
|
||||
}
|
||||
|
||||
getRelaysForPubkey = (pubkey: string, mode?: RelayMode) =>
|
||||
getRelaysFromList(this.get(pubkey), mode)
|
||||
urls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list))
|
||||
|
||||
readUrls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list, RelayMode.Read))
|
||||
|
||||
writeUrls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list, RelayMode.Write))
|
||||
|
||||
// NIP-65 relay-list mutations for the client's user
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import {makeHttpAuth, sendManagementRequest} from "@welshman/util"
|
||||
import type {ManagementRequest} from "@welshman/util"
|
||||
import {User} from "./user.js"
|
||||
import type {IClient} from "./client.js"
|
||||
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
|
||||
@@ -2,7 +2,7 @@ import {groupBy, batch, now, uniq, ago, DAY, HOUR, MINUTE} from "@welshman/lib"
|
||||
import {isOnionUrl, isLocalUrl, isIPAddress, isRelayUrl} from "@welshman/util"
|
||||
import {SocketStatus, SocketEvent} from "@welshman/net"
|
||||
import type {ClientMessage, RelayMessage, Socket} from "@welshman/net"
|
||||
import {ClientData} from "./clientData.js"
|
||||
import {MapPlugin} from "./base.js"
|
||||
import {BlockedRelayLists} from "./blockedRelayLists.js"
|
||||
|
||||
export type RelayStatsUpdate = [string, (stats: RelayStatsItem) => void]
|
||||
@@ -56,7 +56,7 @@ export const makeRelayStatsItem = (url: string): RelayStatsItem => ({
|
||||
* the router uses to rank relays. A pure store — the socket wiring that fills it
|
||||
* lives in `clientPolicyRelayStats`.
|
||||
*/
|
||||
export class RelayStats extends ClientData<RelayStatsItem> {
|
||||
export class RelayStats extends MapPlugin<RelayStatsItem> {
|
||||
getQuality = (url: string) => {
|
||||
// Skip non-relays entirely
|
||||
if (!isRelayUrl(url)) return 0
|
||||
@@ -64,7 +64,7 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
||||
// Skip relays the user has blocked
|
||||
const pubkey = this.ctx.user?.pubkey
|
||||
|
||||
if (pubkey && this.ctx.use(BlockedRelayLists).getBlockedRelays(pubkey).includes(url)) {
|
||||
if (pubkey && this.ctx.use(BlockedRelayLists).urls(pubkey).get().includes(url)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import {derived} from "svelte/store"
|
||||
import {fetchJson} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {displayRelayUrl, displayRelayProfile} from "@welshman/util"
|
||||
import type {RelayProfile} from "@welshman/util"
|
||||
import {LoadableData, projection} from "./clientData.js"
|
||||
import type {Projection} from "./clientData.js"
|
||||
import {LoadableMapPlugin} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
|
||||
/**
|
||||
* NIP-11 relay profiles, keyed by url. A "local" loadable collection: items
|
||||
* aren't nostr events, they're fetched over HTTP from each relay.
|
||||
*/
|
||||
export class Relays extends LoadableData<RelayProfile> {
|
||||
export class Relays extends LoadableMapPlugin<RelayProfile> {
|
||||
fetch = async (url: string): Promise<Maybe<RelayProfile>> => {
|
||||
try {
|
||||
const json = await fetchJson(url.replace(/^ws/, "http"), {
|
||||
@@ -37,9 +36,19 @@ export class Relays extends LoadableData<RelayProfile> {
|
||||
}
|
||||
}
|
||||
|
||||
display = (url: string): Projection<string> => {
|
||||
const read = ($relay: Maybe<RelayProfile>) => displayRelayProfile($relay, displayRelayUrl(url))
|
||||
display = (url: string): Projection<string> =>
|
||||
this.project(url, $relay => displayRelayProfile($relay, displayRelayUrl(url)))
|
||||
|
||||
return projection(derived(this.one(url), read), () => read(this.get(url)))
|
||||
hasNegentropy = async (url: string) => {
|
||||
const relay = await this.load(url)
|
||||
|
||||
if (relay?.negentropy) return true
|
||||
if (relay?.supported_nips?.includes("77")) return true
|
||||
if (relay?.software?.includes?.("strfry") && !relay?.version?.match(/^0\./)) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
hasNip = async (url: string, nip: number | string) =>
|
||||
(await this.load(url))?.supported_nips?.includes(String(nip)) ?? false
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import type {RoomMeta} from "@welshman/util"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {ThunkOptions} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* NIP-29 relay-based group (room) management. Each method publishes the relevant
|
||||
@@ -1,12 +1,8 @@
|
||||
import {RelayMode} from "@welshman/util"
|
||||
import {Router as BaseRouter} from "@welshman/router"
|
||||
import {RelayLists} from "./relayLists.js"
|
||||
import {RelayStats} from "./relayStats.js"
|
||||
import type {IClient} from "./client.js"
|
||||
|
||||
// Re-export the upstream router surface (scenarios, fallback policies,
|
||||
// makeSelection, getFilterSelections, types). The local `Router` below shadows
|
||||
// the upstream `Router` in this re-export.
|
||||
export * from "@welshman/router"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* The upstream `@welshman/router` Router, wired to this client: relay lists come
|
||||
@@ -19,7 +15,11 @@ export class Router extends BaseRouter {
|
||||
constructor(ctx: IClient) {
|
||||
super({
|
||||
getUserPubkey: () => ctx.user?.pubkey,
|
||||
getPubkeyRelays: (pubkey, mode) => ctx.use(RelayLists).getRelaysForPubkey(pubkey, mode),
|
||||
getPubkeyRelays: (pubkey, mode) =>
|
||||
(mode === RelayMode.Read
|
||||
? ctx.use(RelayLists).readUrls(pubkey)
|
||||
: ctx.use(RelayLists).writeUrls(pubkey)
|
||||
).get(),
|
||||
getRelayQuality: url => ctx.use(RelayStats).getQuality(url),
|
||||
getDefaultRelays: ctx.config.getDefaultRelays,
|
||||
getIndexerRelays: ctx.config.getIndexerRelays,
|
||||
@@ -7,7 +7,7 @@ import {dec, inc, sortBy} from "@welshman/lib"
|
||||
import {PROFILE} from "@welshman/util"
|
||||
import type {PublishedProfile, RelayProfile} from "@welshman/util"
|
||||
import {throttled} from "@welshman/store"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {Profiles} from "./profiles.js"
|
||||
+9
-4
@@ -2,25 +2,27 @@ import {
|
||||
SEARCH_RELAYS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
getRelaysFromList,
|
||||
makeList,
|
||||
makeEvent,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {DerivedData} from "./clientData.js"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router} from "./router.js"
|
||||
import {User} from "./user.js"
|
||||
import {User} from "../user.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* NIP-51 search relay lists (kind 10007), keyed by pubkey. Loaded via the
|
||||
* outbox model (the author's write relays), so it depends on the relay-list
|
||||
* collection.
|
||||
*/
|
||||
export class SearchRelayLists extends DerivedData<ReturnType<typeof readList>> {
|
||||
export class SearchRelayLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx, {
|
||||
filters: [{kinds: [SEARCH_RELAYS]}],
|
||||
@@ -33,6 +35,9 @@ export class SearchRelayLists extends DerivedData<ReturnType<typeof readList>> {
|
||||
return this.ctx.use(Network).loadUsingOutbox(pubkey, {kinds: [SEARCH_RELAYS]}, relayHints)
|
||||
}
|
||||
|
||||
urls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list))
|
||||
|
||||
addRelay = async (url: string) => {
|
||||
const user = User.require(this.ctx)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: SEARCH_RELAYS})
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
ItemsByKeyOptions,
|
||||
} from "@welshman/store"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* Store/derivation utilities bound to the client's repository and tracker. Reach
|
||||
@@ -1,6 +1,6 @@
|
||||
import {isSignedEvent} from "@welshman/util"
|
||||
import type {Filter, SignedEvent} from "@welshman/util"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Relays} from "./relays.js"
|
||||
|
||||
@@ -21,25 +21,17 @@ export class Sync {
|
||||
query = (filters: Filter[]) =>
|
||||
this.ctx.repository.query(filters, {shouldSort: filters.every(f => f.limit === undefined)})
|
||||
|
||||
hasNegentropy = (url: string) => {
|
||||
const relay = this.ctx.use(Relays).get(url)
|
||||
|
||||
if (relay?.negentropy) return true
|
||||
if (relay?.supported_nips?.includes?.("77")) return true
|
||||
if (relay?.software?.includes?.("strfry") && !relay?.version?.match(/^0\./)) return true
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
pull = async ({relays, filters}: AppSyncOpts) => {
|
||||
const net = this.ctx.use(Network)
|
||||
const events = this.query(filters).filter(isSignedEvent)
|
||||
|
||||
await Promise.all(
|
||||
relays.map(async relay => {
|
||||
await (this.hasNegentropy(relay)
|
||||
? net.pull({filters, events, relays: [relay]})
|
||||
: net.request({filters, relays: [relay], autoClose: true}))
|
||||
if (await this.ctx.use(Relays).hasNegentropy(relay)) {
|
||||
await net.pull({filters, events, relays: [relay]})
|
||||
} else {
|
||||
await net.request({filters, relays: [relay], autoClose: true})
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -50,9 +42,11 @@ export class Sync {
|
||||
|
||||
await Promise.all(
|
||||
relays.map(async relay => {
|
||||
await (this.hasNegentropy(relay)
|
||||
? net.push({filters, events, relays: [relay]})
|
||||
: Promise.all(events.map((event: SignedEvent) => net.publish({event, relays: [relay]}))))
|
||||
if (await this.ctx.use(Relays).hasNegentropy(relay)) {
|
||||
await net.push({filters, events, relays: [relay]})
|
||||
} else {
|
||||
await Promise.all(events.map((event: SignedEvent) => net.publish({event, relays: [relay]})))
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Router} from "./router.js"
|
||||
import {Profiles} from "./profiles.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
/**
|
||||
* Builders for nostr tags (p/e/a/q/zap/reply/comment/reaction). Needs the router
|
||||
@@ -13,10 +13,11 @@ import {
|
||||
} from "@welshman/util"
|
||||
import {PublishStatus, PublishResult, PublishOptions, PublishResultsByRelay} from "@welshman/net"
|
||||
import {Nip01Signer, Nip59} from "@welshman/signer"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Router, addMinimalFallbacks} from "./router.js"
|
||||
import {User} from "./user.js"
|
||||
import {addMinimalFallbacks} from "@welshman/router"
|
||||
import {Router} from "./router.js"
|
||||
import {User} from "../user.js"
|
||||
|
||||
export type ThunkOptions = Override<
|
||||
PublishOptions,
|
||||
@@ -4,7 +4,7 @@ import {on} from "@welshman/lib"
|
||||
import {getTopicTagValues} from "@welshman/util"
|
||||
import type {RepositoryUpdate} from "@welshman/net"
|
||||
import {deriveItems} from "@welshman/store"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
export type Topic = {
|
||||
name: string
|
||||
@@ -2,9 +2,9 @@ import {readable, derived} from "svelte/store"
|
||||
import {max, throttle, addToMapKey, inc, dec} from "@welshman/lib"
|
||||
import {getListTags, getPubkeyTagValues} from "@welshman/util"
|
||||
import type {List} from "@welshman/util"
|
||||
import type {IClient} from "./client.js"
|
||||
import {projection} from "./clientData.js"
|
||||
import type {Projection} from "./clientData.js"
|
||||
import type {IClient} from "../client.js"
|
||||
import {projection, projectFrom} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
import {FollowLists} from "./follows.js"
|
||||
import {MuteLists} from "./mutes.js"
|
||||
|
||||
@@ -95,24 +95,14 @@ export class Wot {
|
||||
this.max = projection(maxStore)
|
||||
}
|
||||
|
||||
follows = (pubkey: string): Projection<string[]> => {
|
||||
const read = ($lists: ReadonlyMap<string, List>) => listPubkeys($lists.get(pubkey))
|
||||
follows = (pubkey: string): Projection<string[]> =>
|
||||
projectFrom(this.ctx.use(FollowLists).index, $lists => listPubkeys($lists.get(pubkey)))
|
||||
|
||||
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
|
||||
read(this.ctx.use(FollowLists).index.get()),
|
||||
)
|
||||
}
|
||||
mutes = (pubkey: string): Projection<string[]> =>
|
||||
projectFrom(this.ctx.use(MuteLists).index, $lists => listPubkeys($lists.get(pubkey)))
|
||||
|
||||
mutes = (pubkey: string): Projection<string[]> => {
|
||||
const read = ($lists: ReadonlyMap<string, List>) => listPubkeys($lists.get(pubkey))
|
||||
|
||||
return projection(derived(this.ctx.use(MuteLists).index.$, read), () =>
|
||||
read(this.ctx.use(MuteLists).index.get()),
|
||||
)
|
||||
}
|
||||
|
||||
network = (pubkey: string): Projection<string[]> => {
|
||||
const read = ($lists: ReadonlyMap<string, List>) => {
|
||||
network = (pubkey: string): Projection<string[]> =>
|
||||
projectFrom(this.ctx.use(FollowLists).index, $lists => {
|
||||
const pubkeys = new Set(listPubkeys($lists.get(pubkey)))
|
||||
const network = new Set<string>()
|
||||
|
||||
@@ -125,39 +115,20 @@ export class Wot {
|
||||
}
|
||||
|
||||
return Array.from(network)
|
||||
}
|
||||
})
|
||||
|
||||
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
|
||||
read(this.ctx.use(FollowLists).index.get()),
|
||||
)
|
||||
}
|
||||
followers = (pubkey: string): Projection<string[]> =>
|
||||
projectFrom(this.followersByPubkey, $followers => Array.from($followers.get(pubkey) || []))
|
||||
|
||||
followers = (pubkey: string): Projection<string[]> => {
|
||||
const read = ($followers: ReadonlyMap<string, Set<string>>) =>
|
||||
Array.from($followers.get(pubkey) || [])
|
||||
muters = (pubkey: string): Projection<string[]> =>
|
||||
projectFrom(this.mutersByPubkey, $muters => Array.from($muters.get(pubkey) || []))
|
||||
|
||||
return projection(derived(this.followersByPubkey.$, read), () =>
|
||||
read(this.followersByPubkey.get()),
|
||||
)
|
||||
}
|
||||
|
||||
muters = (pubkey: string): Projection<string[]> => {
|
||||
const read = ($muters: ReadonlyMap<string, Set<string>>) =>
|
||||
Array.from($muters.get(pubkey) || [])
|
||||
|
||||
return projection(derived(this.mutersByPubkey.$, read), () => read(this.mutersByPubkey.get()))
|
||||
}
|
||||
|
||||
followsWhoFollow = (pubkey: string, target: string): Projection<string[]> => {
|
||||
const read = ($lists: ReadonlyMap<string, List>) =>
|
||||
followsWhoFollow = (pubkey: string, target: string): Projection<string[]> =>
|
||||
projectFrom(this.ctx.use(FollowLists).index, $lists =>
|
||||
listPubkeys($lists.get(pubkey)).filter(other =>
|
||||
listPubkeys($lists.get(other)).includes(target),
|
||||
)
|
||||
|
||||
return projection(derived(this.ctx.use(FollowLists).index.$, read), () =>
|
||||
read(this.ctx.use(FollowLists).index.get()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
followsWhoMute = (pubkey: string, target: string): Projection<string[]> => {
|
||||
const read = ($follows: ReadonlyMap<string, List>, $mutes: ReadonlyMap<string, List>) =>
|
||||
@@ -5,9 +5,9 @@ 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 {User} from "../user.js"
|
||||
import {MessagingRelayLists} from "./messagingRelayLists.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import type {IClient} from "../client.js"
|
||||
|
||||
export type SendWrappedOptions = Omit<
|
||||
ThunkOptions,
|
||||
@@ -18,13 +18,13 @@ export type SendWrappedOptions = Omit<
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-client gift-wrap (NIP-59) state: the unwrap queue plus failure/dedup
|
||||
* Per-client 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
|
||||
* messages into its own repository — which is what keeps DM history from being
|
||||
* merged across identities. The repository subscription that feeds it lives in
|
||||
* `clientPolicyGiftWraps`.
|
||||
* `clientPolicyWraps`.
|
||||
*/
|
||||
export class GiftWraps {
|
||||
export class Wraps {
|
||||
failedUnwraps = new Set<string>()
|
||||
queue: TaskQueue<TrustedEvent>
|
||||
|
||||
@@ -12,9 +12,9 @@ import type {Maybe} from "@welshman/lib"
|
||||
import {getTagValue, getZapSplits, zapFromEvent} from "@welshman/util"
|
||||
import type {Zapper, Zap, TrustedEvent} from "@welshman/util"
|
||||
import {deriveDeduplicated, deriveDeduplicatedByValue} from "@welshman/store"
|
||||
import {LoadableData, projection} from "./clientData.js"
|
||||
import type {Projection} from "./clientData.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import {LoadableMapPlugin, projection} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
import type {IClient} from "../client.js"
|
||||
import {Profiles} from "./profiles.js"
|
||||
|
||||
/**
|
||||
@@ -23,7 +23,7 @@ import {Profiles} from "./profiles.js"
|
||||
* lnurl, or via a dufflepud proxy to protect user privacy). Depends on the
|
||||
* profiles collection to resolve a pubkey's lnurl.
|
||||
*/
|
||||
export class Zappers extends LoadableData<Zapper> {
|
||||
export class Zappers extends LoadableMapPlugin<Zapper> {
|
||||
fetch = batcher(800, async (lnurls: string[]) => {
|
||||
const result = new Map<string, Zapper>()
|
||||
const valid = lnurls.filter(lnurl => lnurl.startsWith("lnurl1"))
|
||||
@@ -5,9 +5,9 @@ import type {TrustedEvent} from "@welshman/util"
|
||||
import {SocketEvent, isRelayEvent, makeSocketPolicyAuth} from "@welshman/net"
|
||||
import type {RelayMessage, Socket} from "@welshman/net"
|
||||
import type {IClient} from "./client.js"
|
||||
import {RelayStats} from "./relayStats.js"
|
||||
import {GiftWraps} from "./giftWraps.js"
|
||||
import {BlockedRelayLists} from "./blockedRelayLists.js"
|
||||
import {RelayStats} from "./plugins/relayStats.js"
|
||||
import {Wraps} from "./plugins/wraps.js"
|
||||
import {BlockedRelayLists} from "./plugins/blockedRelayLists.js"
|
||||
import {LoggingSigner} from "./logging.js"
|
||||
import type {LogMessage} from "./logging.js"
|
||||
|
||||
@@ -64,7 +64,8 @@ export const clientPolicyAuthUnlessBlocked = makeClientPolicyAuth((socket, clien
|
||||
|
||||
return !client
|
||||
.use(BlockedRelayLists)
|
||||
.getBlockedRelays(client.user.pubkey)
|
||||
.urls(client.user.pubkey)
|
||||
.get()
|
||||
.includes(socket.url)
|
||||
})
|
||||
|
||||
@@ -103,17 +104,17 @@ export const clientPolicyRelayStats: ClientPolicy = client => {
|
||||
* Watches the client's repository for gift wraps (existing and incoming) and
|
||||
* feeds them to the unwrap queue.
|
||||
*/
|
||||
export const clientPolicyGiftWraps: ClientPolicy = client => {
|
||||
const giftWraps = client.use(GiftWraps)
|
||||
export const clientPolicyWraps: ClientPolicy = client => {
|
||||
const wraps = client.use(Wraps)
|
||||
|
||||
for (const wrap of client.repository.query([{kinds: [WRAP]}])) {
|
||||
giftWraps.enqueue(wrap)
|
||||
wraps.enqueue(wrap)
|
||||
}
|
||||
|
||||
return on(client.repository, "update", ({added}: {added: TrustedEvent[]}) => {
|
||||
for (const event of added) {
|
||||
if (event.kind === WRAP) {
|
||||
giftWraps.enqueue(event)
|
||||
wraps.enqueue(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -139,6 +140,6 @@ export const makeClientPolicyLogger =
|
||||
export const defaultClientPolicies: ClientPolicy[] = [
|
||||
clientPolicyIngest,
|
||||
clientPolicyRelayStats,
|
||||
clientPolicyGiftWraps,
|
||||
clientPolicyWraps,
|
||||
clientPolicyAuthUnlessBlocked,
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
build
|
||||
__tests__
|
||||
@@ -0,0 +1,91 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, MUTES, FOLLOWS, getPubkeyTagValues} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {MuteList} from "../src/MuteList"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
|
||||
const a = "aa".repeat(32)
|
||||
const b = "bb".repeat(32)
|
||||
const c = "cc".repeat(32)
|
||||
|
||||
describe("MuteList", () => {
|
||||
it("round-trips public and private mutes through encryption", async () => {
|
||||
const list = MuteList.make().addPublicly(a).addPrivately(b)
|
||||
|
||||
expect(list.pubkeys.sort()).toEqual([a, b].sort())
|
||||
expect(list.includes(a)).toBe(true)
|
||||
expect(list.includes(b)).toBe(true)
|
||||
expect(list.includes(c)).toBe(false)
|
||||
|
||||
const event = await list.toEvent(signer)
|
||||
|
||||
expect(event.kind).toBe(MUTES)
|
||||
expect(event.sig).toBeTruthy()
|
||||
// Public entry is visible in tags; private entry is encrypted in content.
|
||||
expect(getPubkeyTagValues(event.tags)).toEqual([a])
|
||||
expect(event.content).not.toBe("")
|
||||
|
||||
// Re-parsing with a capable signer recovers the private entries.
|
||||
const decrypted = await MuteList.parse(event, signer)
|
||||
|
||||
expect(decrypted.isDecrypted).toBe(true)
|
||||
expect(decrypted.pubkeys.sort()).toEqual([a, b].sort())
|
||||
|
||||
// Parsing without a signer exposes only the public entries.
|
||||
const publicOnly = await MuteList.parse(event)
|
||||
|
||||
expect(publicOnly.isDecrypted).toBe(false)
|
||||
expect(publicOnly.pubkeys).toEqual([a])
|
||||
})
|
||||
|
||||
it("removes from both public and private entries", async () => {
|
||||
const list = MuteList.make().addPublicly(a).addPrivately(b)
|
||||
|
||||
list.remove(a)
|
||||
list.remove(b)
|
||||
|
||||
expect(list.pubkeys).toEqual([])
|
||||
})
|
||||
|
||||
it("preserves undecrypted ciphertext on pass-through serialization", async () => {
|
||||
const event = await MuteList.make().addPrivately(b).toEvent(signer)
|
||||
const undecrypted = await MuteList.parse(event)
|
||||
|
||||
// We never decrypted, so the original ciphertext must survive untouched.
|
||||
const template = await undecrypted.getTemplate(signer)
|
||||
|
||||
expect(template.content).toBe(event.content)
|
||||
})
|
||||
|
||||
it("refuses private mutation when undecrypted", async () => {
|
||||
const event = await MuteList.make().addPrivately(b).toEvent(signer)
|
||||
const undecrypted = await MuteList.parse(event)
|
||||
|
||||
expect(() => undecrypted.addPrivately(c)).toThrow()
|
||||
})
|
||||
|
||||
it("toRumor encrypts but does not sign", async () => {
|
||||
const rumor = await MuteList.make().addPrivately(b).toRumor(signer)
|
||||
|
||||
expect(rumor.id).toBeTruthy()
|
||||
expect((rumor as TrustedEvent).sig).toBeUndefined()
|
||||
expect(rumor.content).not.toBe("")
|
||||
})
|
||||
|
||||
it("serializes to JSON", async () => {
|
||||
const list = MuteList.make().addPublicly(a).addPrivately(b)
|
||||
const json = JSON.parse(JSON.stringify(list))
|
||||
|
||||
expect(json.kind).toBe(MUTES)
|
||||
expect(json.publicTags).toEqual([["p", a]])
|
||||
expect(json.privateTags).toEqual([["p", b]])
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", async () => {
|
||||
const event = {kind: FOLLOWS, tags: [], content: "", pubkey: a} as TrustedEvent
|
||||
|
||||
await expect(MuteList.parse(event)).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import {describe, it, expect} from "vitest"
|
||||
import {makeSecret, PROFILE, NOTE} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {Nip01Signer} from "@welshman/signer"
|
||||
import {Profile, displayPubkey} from "../src/Profile"
|
||||
|
||||
const signer = new Nip01Signer(makeSecret())
|
||||
const pubkey = "ee".repeat(32)
|
||||
|
||||
const makeEvent = (overrides: Partial<TrustedEvent> = {}): TrustedEvent =>
|
||||
({
|
||||
id: "ff".repeat(32),
|
||||
pubkey,
|
||||
created_at: 0,
|
||||
kind: PROFILE,
|
||||
tags: [],
|
||||
content: "",
|
||||
sig: "00".repeat(64),
|
||||
...overrides,
|
||||
}) as TrustedEvent
|
||||
|
||||
describe("Profile", () => {
|
||||
it("parses and re-signs profile content", async () => {
|
||||
const event = makeEvent({
|
||||
content: JSON.stringify({name: "alice", about: "hi"}),
|
||||
tags: [["alt", "profile"]],
|
||||
})
|
||||
|
||||
const profile = Profile.parse(event)
|
||||
|
||||
expect(profile.values.name).toBe("alice")
|
||||
expect(profile.hasName()).toBe(true)
|
||||
expect(profile.display()).toBe("alice")
|
||||
|
||||
const signed = await profile.toEvent(signer)
|
||||
|
||||
expect(signed.kind).toBe(PROFILE)
|
||||
expect(JSON.parse(signed.content).name).toBe("alice")
|
||||
// Source tags are preserved.
|
||||
expect(signed.tags).toEqual([["alt", "profile"]])
|
||||
})
|
||||
|
||||
it("derives lnurl from a lud16 address", () => {
|
||||
const profile = Profile.make({lud16: "alice@example.com"})
|
||||
|
||||
expect(profile.values.lnurl).toBeTruthy()
|
||||
})
|
||||
|
||||
it("set merges and re-derives values", () => {
|
||||
const profile = Profile.make({name: "alice"})
|
||||
|
||||
profile.set({about: "hello"})
|
||||
|
||||
expect(profile.values.name).toBe("alice")
|
||||
expect(profile.values.about).toBe("hello")
|
||||
})
|
||||
|
||||
it("display falls back to a shortened npub", () => {
|
||||
const profile = Profile.parse(makeEvent({content: "{}"}))
|
||||
|
||||
expect(profile.display()).toBe(displayPubkey(pubkey))
|
||||
})
|
||||
|
||||
it("serializes to JSON", () => {
|
||||
const profile = Profile.make({name: "alice"})
|
||||
|
||||
expect(JSON.parse(JSON.stringify(profile))).toEqual({name: "alice"})
|
||||
})
|
||||
|
||||
it("throws on the wrong kind", () => {
|
||||
expect(() => Profile.parse(makeEvent({kind: NOTE}))).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "@welshman/domain",
|
||||
"version": "0.8.16",
|
||||
"author": "hodlbod",
|
||||
"license": "MIT",
|
||||
"description": "Stateless utilities for translating nostr events to and from domain objects.",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "dist/domain/src/index.js",
|
||||
"types": "dist/domain/src/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm run clean && pnpm run compile --force",
|
||||
"clean": "rimraf ./dist",
|
||||
"compile": "tsc -b tsconfig.build.json",
|
||||
"prepublishOnly": "pnpm run build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@welshman/lib": "workspace:*",
|
||||
"@welshman/signer": "workspace:*",
|
||||
"@welshman/util": "workspace:*",
|
||||
"nostr-tools": "^2.19.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@welshman/lib": "workspace:*",
|
||||
"@welshman/signer": "workspace:*",
|
||||
"@welshman/util": "workspace:*",
|
||||
"nostr-tools": "^2.19.4",
|
||||
"rimraf": "~6.0.0",
|
||||
"typescript": "~5.8.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import {nthEq, parseJson} from "@welshman/lib"
|
||||
import {uniqTags} from "@welshman/util"
|
||||
import type {EventTemplate, TrustedEvent} from "@welshman/util"
|
||||
import {decrypt} from "@welshman/signer"
|
||||
import type {ISigner} from "@welshman/signer"
|
||||
import {DomainObject} from "./base.js"
|
||||
|
||||
const isValidTag = (tag: unknown): tag is string[] =>
|
||||
Array.isArray(tag) && tag.length > 0 && tag.every(v => typeof v === "string")
|
||||
|
||||
export type DecryptedTags = {
|
||||
privateTags: string[][]
|
||||
// True when the private content was read (or there was none), false when we
|
||||
// hold ciphertext we couldn't decrypt. See `EncryptableList.isDecrypted`.
|
||||
decrypted: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and decrypt the private tags stored in an event's content. Returns
|
||||
* `decrypted: false` (and leaves `privateTags` empty) when there is encrypted
|
||||
* content but no signer, or when decryption fails — in that case the original
|
||||
* ciphertext is preserved verbatim on serialization.
|
||||
*/
|
||||
export const decryptListContent = async (
|
||||
event: TrustedEvent,
|
||||
signer?: ISigner,
|
||||
): Promise<DecryptedTags> => {
|
||||
// No private content to read.
|
||||
if (!event.content) return {privateTags: [], decrypted: true}
|
||||
|
||||
// No signer to read it with — keep the ciphertext, mark it undecrypted.
|
||||
if (!signer) return {privateTags: [], decrypted: false}
|
||||
|
||||
try {
|
||||
const plaintext = await decrypt(signer, event.pubkey, event.content)
|
||||
const privateTags = (parseJson(plaintext) || []).filter(isValidTag)
|
||||
|
||||
return {privateTags, decrypted: true}
|
||||
} catch {
|
||||
return {privateTags: [], decrypted: false}
|
||||
}
|
||||
}
|
||||
|
||||
export type EncryptableListParams = {
|
||||
publicTags?: string[][]
|
||||
privateTags?: string[][]
|
||||
decrypted?: boolean
|
||||
event?: TrustedEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for replaceable lists that carry public entries in tags and
|
||||
* private entries as an encrypted JSON array in content (NIP-51 style). The
|
||||
* private entries are decrypted to plaintext on `parse` and re-encrypted on
|
||||
* `getTemplate`, so all in-between reads and writes are synchronous.
|
||||
*
|
||||
* Subclasses fix the `kind` and add domain-specific accessors (see
|
||||
* `MuteList`). The generic tag mechanics live here.
|
||||
*/
|
||||
export abstract class EncryptableList extends DomainObject {
|
||||
abstract readonly kind: number
|
||||
|
||||
publicTags: string[][]
|
||||
privateTags: string[][]
|
||||
readonly event?: TrustedEvent
|
||||
|
||||
// Whether `privateTags` reflects the real (decrypted) private content. False
|
||||
// means we're holding ciphertext we couldn't read, so private entries are
|
||||
// unknown and must not be mutated.
|
||||
protected decrypted: boolean
|
||||
|
||||
constructor({
|
||||
publicTags = [],
|
||||
privateTags = [],
|
||||
decrypted = true,
|
||||
event,
|
||||
}: EncryptableListParams = {}) {
|
||||
super()
|
||||
|
||||
this.publicTags = publicTags
|
||||
this.privateTags = privateTags
|
||||
this.decrypted = decrypted
|
||||
this.event = event
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the private entries were successfully decrypted (or there were
|
||||
* none). When false, only public entries are available and private mutations
|
||||
* throw.
|
||||
*/
|
||||
get isDecrypted() {
|
||||
return this.decrypted
|
||||
}
|
||||
|
||||
/** All entries, merging public and (when decrypted) private tags. */
|
||||
getTags() {
|
||||
return [...this.publicTags, ...this.privateTags]
|
||||
}
|
||||
|
||||
getPublicTags() {
|
||||
return this.publicTags
|
||||
}
|
||||
|
||||
getPrivateTags() {
|
||||
return this.privateTags
|
||||
}
|
||||
|
||||
/** Add one or more tags to the public (cleartext) entries. */
|
||||
addPublicTags(...tags: string[][]) {
|
||||
this.publicTags = uniqTags([...this.publicTags, ...tags])
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/** Add one or more tags to the private (encrypted) entries. */
|
||||
addPrivateTags(...tags: string[][]) {
|
||||
this.assertDecrypted()
|
||||
|
||||
this.privateTags = uniqTags([...this.privateTags, ...tags])
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/** Remove every tag matching `pred` from both public and private entries. */
|
||||
removeTagsBy(pred: (tag: string[]) => boolean) {
|
||||
this.publicTags = this.publicTags.filter(t => !pred(t))
|
||||
|
||||
if (this.decrypted) {
|
||||
this.privateTags = this.privateTags.filter(t => !pred(t))
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/** Remove every tag whose value (index 1) equals `value`, public or private. */
|
||||
removeTagsByValue(value: string) {
|
||||
return this.removeTagsBy(nthEq(1, value))
|
||||
}
|
||||
|
||||
protected assertDecrypted() {
|
||||
if (!this.decrypted) {
|
||||
throw new Error("Cannot modify the private entries of a list that has not been decrypted")
|
||||
}
|
||||
}
|
||||
|
||||
async getTemplate(signer?: ISigner): Promise<EventTemplate> {
|
||||
const tags = this.publicTags
|
||||
|
||||
// Preserve the original ciphertext when we never decrypted it, so a
|
||||
// pass-through round trip doesn't destroy private entries we can't read.
|
||||
let content = this.event?.content || ""
|
||||
|
||||
if (this.decrypted) {
|
||||
if (this.privateTags.length === 0) {
|
||||
content = ""
|
||||
} else {
|
||||
if (!signer) {
|
||||
throw new Error("A signer is required to encrypt the private entries of a list")
|
||||
}
|
||||
|
||||
const pubkey = await signer.getPubkey()
|
||||
|
||||
content = await signer.nip44.encrypt(pubkey, JSON.stringify(this.privateTags))
|
||||
}
|
||||
}
|
||||
|
||||
return {kind: this.kind, tags, content}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
kind: this.kind,
|
||||
publicTags: this.publicTags,
|
||||
privateTags: this.privateTags,
|
||||
decrypted: this.decrypted,
|
||||
event: this.event,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import {uniq} from "@welshman/lib"
|
||||
import {MUTES, getPubkeyTagValues} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import type {ISigner} from "@welshman/signer"
|
||||
import {EncryptableList, decryptListContent} from "./List.js"
|
||||
|
||||
/**
|
||||
* A NIP-51 kind-10000 mute list. Pubkeys can be muted publicly (in tags) or
|
||||
* privately (in encrypted content); the convenience accessors here treat both
|
||||
* as one merged set.
|
||||
*
|
||||
* @example
|
||||
* const mutes = await MuteList.parse(event, signer) // decrypts if able
|
||||
* mutes.addPrivately(pubkey1)
|
||||
* mutes.remove(pubkey2) // public and private
|
||||
* mutes.pubkeys // merged
|
||||
* mutes.includes(pubkey)
|
||||
* const signed = await mutes.toEvent(signer) // encrypts + signs
|
||||
*/
|
||||
export class MuteList extends EncryptableList {
|
||||
readonly kind = MUTES
|
||||
|
||||
/** Create an empty, decrypted mute list (e.g. for a user with none yet). */
|
||||
static make() {
|
||||
return new MuteList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a kind-10000 event into a `MuteList`, decrypting its private entries
|
||||
* when a capable signer is supplied. Throws on the wrong kind.
|
||||
*/
|
||||
static async parse(event: TrustedEvent, signer?: ISigner) {
|
||||
if (event.kind !== MUTES) {
|
||||
throw new Error(`Expected a kind ${MUTES} event, got kind ${event.kind}`)
|
||||
}
|
||||
|
||||
const {privateTags, decrypted} = await decryptListContent(event, signer)
|
||||
|
||||
return new MuteList({event, publicTags: event.tags, privateTags, decrypted})
|
||||
}
|
||||
|
||||
/** The muted pubkeys, merging public and (when decrypted) private entries. */
|
||||
get pubkeys() {
|
||||
return uniq(getPubkeyTagValues(this.getTags()))
|
||||
}
|
||||
|
||||
/** Whether `pubkey` is muted, publicly or privately. */
|
||||
includes(pubkey: string) {
|
||||
return this.pubkeys.includes(pubkey)
|
||||
}
|
||||
|
||||
/** Mute a pubkey publicly (visible to anyone who reads the event). */
|
||||
addPublicly(pubkey: string) {
|
||||
return this.addPublicTags(["p", pubkey])
|
||||
}
|
||||
|
||||
/** Mute a pubkey privately (stored in encrypted content). */
|
||||
addPrivately(pubkey: string) {
|
||||
return this.addPrivateTags(["p", pubkey])
|
||||
}
|
||||
|
||||
/** Unmute a pubkey, removing it from both public and private entries. */
|
||||
remove(pubkey: string) {
|
||||
return this.removeTagsByValue(pubkey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import {npubEncode} from "nostr-tools/nip19"
|
||||
import {ellipsize, parseJson} from "@welshman/lib"
|
||||
import {PROFILE, getLnUrl} from "@welshman/util"
|
||||
import type {EventTemplate, TrustedEvent} from "@welshman/util"
|
||||
import {DomainObject} from "./base.js"
|
||||
|
||||
export type ProfileValues = {
|
||||
name?: string
|
||||
nip05?: string
|
||||
lud06?: string
|
||||
lud16?: string
|
||||
lnurl?: string
|
||||
about?: string
|
||||
banner?: string
|
||||
picture?: string
|
||||
website?: string
|
||||
display_name?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize raw profile values, deriving `lnurl` from a `lud06` or `lud16`
|
||||
* address when present.
|
||||
*/
|
||||
export const makeProfileValues = (values: Partial<ProfileValues> = {}): ProfileValues => {
|
||||
const result: ProfileValues = {...values}
|
||||
|
||||
for (const key of ["lud06", "lud16"] as const) {
|
||||
if (typeof result[key] === "string") {
|
||||
const lnurl = getLnUrl(result[key]!)
|
||||
|
||||
if (lnurl) {
|
||||
result.lnurl = lnurl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export const displayPubkey = (pubkey: string) => {
|
||||
const d = npubEncode(pubkey)
|
||||
|
||||
return d.slice(0, 8) + "…" + d.slice(-5)
|
||||
}
|
||||
|
||||
/**
|
||||
* A kind-0 profile. Profile data lives unencrypted in the event content as
|
||||
* JSON, so this is the simplest kind of domain object — `getTemplate` ignores
|
||||
* the signer and only `toEvent`/`toRumor` need one (to sign).
|
||||
*
|
||||
* @example
|
||||
* const profile = Profile.parse(event)
|
||||
* profile.set({about: "hello"})
|
||||
* const signed = await profile.toEvent(signer)
|
||||
*/
|
||||
export class Profile extends DomainObject {
|
||||
readonly kind = PROFILE
|
||||
readonly event?: TrustedEvent
|
||||
|
||||
values: ProfileValues
|
||||
|
||||
constructor(values: Partial<ProfileValues> = {}, event?: TrustedEvent) {
|
||||
super()
|
||||
|
||||
this.values = makeProfileValues(values)
|
||||
this.event = event
|
||||
}
|
||||
|
||||
static make(values: Partial<ProfileValues> = {}) {
|
||||
return new Profile(values)
|
||||
}
|
||||
|
||||
/** Parse a kind-0 event into a `Profile`. Throws on the wrong kind. */
|
||||
static parse(event: TrustedEvent) {
|
||||
if (event.kind !== PROFILE) {
|
||||
throw new Error(`Expected a kind ${PROFILE} event, got kind ${event.kind}`)
|
||||
}
|
||||
|
||||
return new Profile(parseJson(event.content) || {}, event)
|
||||
}
|
||||
|
||||
/** Merge `updates` into the profile values, re-deriving `lnurl` as needed. */
|
||||
set(updates: Partial<ProfileValues>) {
|
||||
this.values = makeProfileValues({...this.values, ...updates})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/** Whether the profile has a display-worthy name. */
|
||||
hasName() {
|
||||
return Boolean(this.values.name || this.values.display_name)
|
||||
}
|
||||
|
||||
/** A human-readable label, falling back to a shortened npub, then `fallback`. */
|
||||
display(fallback = "") {
|
||||
const {name, display_name} = this.values
|
||||
|
||||
if (name) return ellipsize(name, 60).trim()
|
||||
if (display_name) return ellipsize(display_name, 60).trim()
|
||||
if (this.event) return displayPubkey(this.event.pubkey).trim()
|
||||
|
||||
return fallback.trim()
|
||||
}
|
||||
|
||||
async getTemplate(): Promise<EventTemplate> {
|
||||
return {
|
||||
kind: PROFILE,
|
||||
content: JSON.stringify(this.values),
|
||||
// Preserve any tags from the source event (e.g. nip05/relay hints).
|
||||
tags: this.event?.tags || [],
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {...this.values}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import {stamp, prep} from "@welshman/util"
|
||||
import type {EventTemplate, SignedEvent, HashedEvent, TrustedEvent} from "@welshman/util"
|
||||
import type {ISigner} from "@welshman/signer"
|
||||
|
||||
/**
|
||||
* The base class for domain objects.
|
||||
*
|
||||
* A domain object is an in-memory, mutable, JSON-serializable view of a single
|
||||
* nostr event. The pattern is "decrypt on parse, mutate in memory, encrypt on
|
||||
* serialize": concrete subclasses decrypt private content up front (in their
|
||||
* static `parse`), expose synchronous accessors and mutators over the
|
||||
* plaintext, and only touch the signer again when building an event.
|
||||
*
|
||||
* Subclasses provide:
|
||||
*
|
||||
* - a static `parse(event, signer?)` that reads (and, when possible, decrypts)
|
||||
* an event into a domain object
|
||||
* - `getTemplate(signer?)` that builds (and, when needed, encrypts) the event
|
||||
* template — the signer is optional for objects with no private content
|
||||
* - `toJSON()` for plain-object serialization
|
||||
*
|
||||
* The base provides the shared signing/wrapping orchestration on top of
|
||||
* `getTemplate`.
|
||||
*/
|
||||
export abstract class DomainObject {
|
||||
/**
|
||||
* The source event, present when this object was parsed from one and absent
|
||||
* when it was freshly constructed.
|
||||
*/
|
||||
abstract readonly event?: TrustedEvent
|
||||
|
||||
/**
|
||||
* Build the event template for this object, encrypting any private content
|
||||
* with the signer. Subclasses that hold no private data may ignore the
|
||||
* signer (which is why it is optional).
|
||||
*/
|
||||
abstract getTemplate(signer?: ISigner): Promise<EventTemplate>
|
||||
|
||||
/**
|
||||
* A plain-JSON snapshot of this object — safe for storage, `structuredClone`,
|
||||
* or `postMessage`. Also lets `JSON.stringify` work transparently.
|
||||
*/
|
||||
abstract toJSON(): object
|
||||
|
||||
/**
|
||||
* Build a hashed-but-unsigned rumor (the inner event of a NIP-59 gift wrap),
|
||||
* encrypting private content as needed. A fresh `created_at` is stamped.
|
||||
*/
|
||||
async toRumor(signer: ISigner): Promise<HashedEvent> {
|
||||
const [template, pubkey] = await Promise.all([this.getTemplate(signer), signer.getPubkey()])
|
||||
|
||||
return prep(template, pubkey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and sign a full event, encrypting private content as needed. A fresh
|
||||
* `created_at` is stamped so the result supersedes any prior version.
|
||||
*/
|
||||
async toEvent(signer: ISigner): Promise<SignedEvent> {
|
||||
const template = await this.getTemplate(signer)
|
||||
|
||||
return signer.sign(stamp(template))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./base.js"
|
||||
export * from "./List.js"
|
||||
export * from "./MuteList.js"
|
||||
export * from "./Profile.js"
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "../../tsconfig.build.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"@welshman/lib": ["../lib/src/index.js"],
|
||||
"@welshman/util": ["../util/src/index.js"],
|
||||
"@welshman/net": ["../net/src/index.js"],
|
||||
"@welshman/signer": ["../signer/src/index.js"]
|
||||
}
|
||||
},
|
||||
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
Generated
+21
@@ -172,6 +172,27 @@ importers:
|
||||
specifier: ~5.8.0
|
||||
version: 5.8.2
|
||||
|
||||
packages/domain:
|
||||
devDependencies:
|
||||
'@welshman/lib':
|
||||
specifier: workspace:*
|
||||
version: link:../lib
|
||||
'@welshman/signer':
|
||||
specifier: workspace:*
|
||||
version: link:../signer
|
||||
'@welshman/util':
|
||||
specifier: workspace:*
|
||||
version: link:../util
|
||||
nostr-tools:
|
||||
specifier: ^2.19.4
|
||||
version: 2.19.4(typescript@5.8.2)
|
||||
rimraf:
|
||||
specifier: ~6.0.0
|
||||
version: 6.0.1
|
||||
typescript:
|
||||
specifier: ~5.8.0
|
||||
version: 5.8.2
|
||||
|
||||
packages/editor:
|
||||
dependencies:
|
||||
'@tiptap/core':
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"references": [
|
||||
{"path": "./packages/app/tsconfig.build.json"},
|
||||
{"path": "./packages/content/tsconfig.build.json"},
|
||||
{"path": "./packages/domain/tsconfig.build.json"},
|
||||
{"path": "./packages/dvm/tsconfig.build.json"},
|
||||
{"path": "./packages/editor/tsconfig.build.json"},
|
||||
{"path": "./packages/feeds/tsconfig.build.json"},
|
||||
|
||||
@@ -15,6 +15,7 @@ export default defineConfig({
|
||||
alias: {
|
||||
"@welshman/app": resolve(__dirname, "packages/app/src"),
|
||||
"@welshman/content": resolve(__dirname, "packages/content/src"),
|
||||
"@welshman/domain": resolve(__dirname, "packages/domain/src"),
|
||||
"@welshman/feeds": resolve(__dirname, "packages/feeds/src"),
|
||||
"@welshman/lib": resolve(__dirname, "packages/lib/src"),
|
||||
"@welshman/net": resolve(__dirname, "packages/net/src"),
|
||||
|
||||
Reference in New Issue
Block a user