This commit is contained in:
@@ -25,6 +25,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pomade/core": "^0.2.1",
|
||||
"@welshman/domain": "workspace:*",
|
||||
"@welshman/feeds": "workspace:*",
|
||||
"@welshman/lib": "workspace:*",
|
||||
"@welshman/net": "workspace:*",
|
||||
@@ -39,6 +40,7 @@
|
||||
"typescript": "~5.8.0",
|
||||
"@pomade/core": "^0.2.1",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@welshman/domain": "workspace:*",
|
||||
"@welshman/feeds": "workspace:*",
|
||||
"@welshman/lib": "workspace:*",
|
||||
"@welshman/net": "workspace:*",
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import {
|
||||
BLOCKED_RELAYS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
getRelaysFromList,
|
||||
makeList,
|
||||
makeEvent,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {BLOCKED_RELAYS} from "@welshman/util"
|
||||
import {BlockedRelayList, BlockedRelayListBuilder} from "@welshman/domain"
|
||||
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 {Thunks} from "./thunk.js"
|
||||
import type {IApp} from "../app.js"
|
||||
@@ -22,12 +12,12 @@ import type {IApp} from "../app.js"
|
||||
* so it depends on the relay-list collection. Feeds `RelayStats.getQuality` so
|
||||
* blocked relays are never selected.
|
||||
*/
|
||||
export class BlockedRelayLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
export class BlockedRelayLists extends DerivedPlugin<BlockedRelayList> {
|
||||
constructor(app: IApp) {
|
||||
super(app, {
|
||||
filters: [{kinds: [BLOCKED_RELAYS]}],
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
getKey: list => list.event.pubkey,
|
||||
eventToItem: BlockedRelayList.factory(app.user?.signer),
|
||||
getKey: list => list.author(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,27 +26,22 @@ export class BlockedRelayLists extends DerivedPlugin<ReturnType<typeof readList>
|
||||
}
|
||||
|
||||
urls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list))
|
||||
this.project(pubkey, list => list?.urls() ?? [])
|
||||
|
||||
addRelay = async (url: string) => {
|
||||
update = async (fn: (builder: BlockedRelayListBuilder) => void) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: BLOCKED_RELAYS})
|
||||
const event = await addToListPublicly(list, ["relay", url]).reconcile(user.nip44EncryptToSelf)
|
||||
const builder = new BlockedRelayListBuilder(await this.forceLoad(user.pubkey))
|
||||
|
||||
fn(builder)
|
||||
|
||||
const event = await builder.toTemplate(user.signer)
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
removeRelay = async (url: string) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: BLOCKED_RELAYS})
|
||||
const event = await removeFromList(list, url).reconcile(user.nip44EncryptToSelf)
|
||||
addUrl = (url: string) => this.update(builder => builder.addUrl(url))
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
removeUrl = (url: string) => this.update(builder => builder.removeUrl(url))
|
||||
|
||||
setRelays = (urls: string[]) =>
|
||||
this.app.use(Thunks).publish({
|
||||
event: makeEvent(BLOCKED_RELAYS, {tags: urls.map(url => ["relay", url])}),
|
||||
relays: this.app.use(Router).FromUser().getUrls(),
|
||||
})
|
||||
setUrls = (urls: string[]) => this.update(builder => builder.setUrls(urls))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {BLOSSOM_SERVERS, asDecryptedEvent, readList} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {BLOSSOM_SERVERS} from "@welshman/util"
|
||||
import {BlossomServerList} from "@welshman/domain"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import {Network} from "./network.js"
|
||||
import type {IApp} from "../app.js"
|
||||
@@ -8,12 +8,12 @@ import type {IApp} from "../app.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 DerivedPlugin<ReturnType<typeof readList>> {
|
||||
export class BlossomServerLists extends DerivedPlugin<BlossomServerList> {
|
||||
constructor(app: IApp) {
|
||||
super(app, {
|
||||
filters: [{kinds: [BLOSSOM_SERVERS]}],
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
getKey: list => list.event.pubkey,
|
||||
eventToItem: BlossomServerList.factory(app.user?.signer),
|
||||
getKey: list => list.author(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import {
|
||||
FOLLOWS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
makeList,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {FOLLOWS} from "@welshman/util"
|
||||
import {FollowList, FollowListBuilder} from "@welshman/domain"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
@@ -17,12 +10,12 @@ import type {IApp} from "../app.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 DerivedPlugin<ReturnType<typeof readList>> {
|
||||
export class FollowLists extends DerivedPlugin<FollowList> {
|
||||
constructor(app: IApp) {
|
||||
super(app, {
|
||||
filters: [{kinds: [FOLLOWS]}],
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
getKey: followList => followList.event.pubkey,
|
||||
eventToItem: FollowList.factory(app.user?.signer),
|
||||
getKey: followList => followList.author(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,19 +23,18 @@ export class FollowLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
return this.app.use(Network).loadUsingOutbox(pubkey, {kinds: [FOLLOWS]}, relayHints)
|
||||
}
|
||||
|
||||
follow = async (tag: string[]) => {
|
||||
update = async (fn: (builder: FollowListBuilder) => void) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: FOLLOWS})
|
||||
const event = await addToListPublicly(list, tag).reconcile(user.nip44EncryptToSelf)
|
||||
const builder = new FollowListBuilder(await this.forceLoad(user.pubkey))
|
||||
|
||||
fn(builder)
|
||||
|
||||
const event = await builder.toTemplate(user.signer)
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
unfollow = async (value: string) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: FOLLOWS})
|
||||
const event = await removeFromList(list, value).reconcile(user.nip44EncryptToSelf)
|
||||
follow = (tag: string[]) => this.update(builder => builder.addPublic(tag))
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
unfollow = (value: string) => this.update(builder => builder.removeFollow(value))
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {tryCatch, batcher, postJson} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {queryProfile, displayNip05} from "@welshman/util"
|
||||
import type {Handle} from "@welshman/util"
|
||||
import type {Profile} from "@welshman/domain"
|
||||
import {deriveDeduplicated} from "@welshman/store"
|
||||
import {LoadableMapPlugin, projection} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
@@ -60,16 +61,20 @@ export class Handles extends LoadableMapPlugin<Handle> {
|
||||
loadForPubkey = async (pubkey: string, relays: string[] = []) => {
|
||||
const $profile = await this.app.use(Profiles).load(pubkey, relays)
|
||||
|
||||
return $profile?.nip05 ? this.load($profile.nip05) : undefined
|
||||
const nip05 = $profile?.nip05()
|
||||
|
||||
return nip05 ? this.load(nip05) : undefined
|
||||
}
|
||||
|
||||
forPubkey = (pubkey: string, relays: string[] = []): Projection<Maybe<Handle>> => {
|
||||
this.loadForPubkey(pubkey, relays)
|
||||
|
||||
const read = ([$handlesByNip05, $profile]: [ReadonlyMap<string, Handle>, Maybe<{nip05?: string}>]) => {
|
||||
if (!$profile?.nip05) return undefined
|
||||
const read = ([$handlesByNip05, $profile]: [ReadonlyMap<string, Handle>, Maybe<Profile>]) => {
|
||||
const nip05 = $profile?.nip05()
|
||||
|
||||
const handle = $handlesByNip05.get($profile.nip05)
|
||||
if (!nip05) return undefined
|
||||
|
||||
const handle = $handlesByNip05.get(nip05)
|
||||
|
||||
if (handle?.pubkey !== pubkey) return undefined
|
||||
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import {
|
||||
MESSAGING_RELAYS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
getRelaysFromList,
|
||||
makeList,
|
||||
makeEvent,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {MESSAGING_RELAYS} from "@welshman/util"
|
||||
import {MessagingRelayList, MessagingRelayListBuilder} from "@welshman/domain"
|
||||
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 {Thunks} from "./thunk.js"
|
||||
import type {IApp} from "../app.js"
|
||||
@@ -22,12 +12,12 @@ import type {IApp} from "../app.js"
|
||||
* outbox model (the author's write relays), so it depends on the relay-list
|
||||
* collection.
|
||||
*/
|
||||
export class MessagingRelayLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
export class MessagingRelayLists extends DerivedPlugin<MessagingRelayList> {
|
||||
constructor(app: IApp) {
|
||||
super(app, {
|
||||
filters: [{kinds: [MESSAGING_RELAYS]}],
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
getKey: list => list.event.pubkey,
|
||||
eventToItem: MessagingRelayList.factory(app.user?.signer),
|
||||
getKey: list => list.author(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,27 +26,22 @@ export class MessagingRelayLists extends DerivedPlugin<ReturnType<typeof readLis
|
||||
}
|
||||
|
||||
urls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list))
|
||||
this.project(pubkey, list => list?.urls() ?? [])
|
||||
|
||||
addRelay = async (url: string) => {
|
||||
update = async (fn: (builder: MessagingRelayListBuilder) => void) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MESSAGING_RELAYS})
|
||||
const event = await addToListPublicly(list, ["relay", url]).reconcile(user.nip44EncryptToSelf)
|
||||
const builder = new MessagingRelayListBuilder(await this.forceLoad(user.pubkey))
|
||||
|
||||
fn(builder)
|
||||
|
||||
const event = await builder.toTemplate(user.signer)
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
removeRelay = async (url: string) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MESSAGING_RELAYS})
|
||||
const event = await removeFromList(list, url).reconcile(user.nip44EncryptToSelf)
|
||||
addUrl = (url: string) => this.update(builder => builder.addUrl(url))
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
removeUrl = (url: string) => this.update(builder => builder.removeUrl(url))
|
||||
|
||||
setRelays = (urls: string[]) =>
|
||||
this.app.use(Thunks).publish({
|
||||
event: makeEvent(MESSAGING_RELAYS, {tags: urls.map(url => ["relay", url])}),
|
||||
relays: this.app.use(Router).FromUser().getUrls(),
|
||||
})
|
||||
setUrls = (urls: string[]) => this.update(builder => builder.setUrls(urls))
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import {
|
||||
MUTES,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
makeList,
|
||||
addToListPublicly,
|
||||
addToListPrivately,
|
||||
removeFromList,
|
||||
updateList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, PublishedList} from "@welshman/util"
|
||||
import {nthEq} from "@welshman/lib"
|
||||
import {MUTES} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import type {ISigner} from "@welshman/signer"
|
||||
import {MuteList, MuteListBuilder} from "@welshman/domain"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import type {IApp} from "../app.js"
|
||||
import {Network} from "./network.js"
|
||||
@@ -17,19 +11,47 @@ import {Plaintext} from "./plaintext.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.
|
||||
* A signer that decrypts via the app's plaintext cache (keyed by event), falling
|
||||
* back to the real signer. Lets `MuteList.fromEvent(event, signer)` reuse cached
|
||||
* decryptions instead of re-decrypting. Returns undefined when there's no user,
|
||||
* so the reader falls back to public-only.
|
||||
*/
|
||||
export class MuteLists extends DerivedPlugin<PublishedList> {
|
||||
const makeCachedSigner = (app: IApp, event: TrustedEvent): ISigner | undefined => {
|
||||
const user = app.user
|
||||
|
||||
if (!user) return undefined
|
||||
|
||||
const {signer} = user
|
||||
const decryptVia =
|
||||
(fallback: (pubkey: string, message: string) => Promise<string>) =>
|
||||
async (pubkey: string, message: string) =>
|
||||
(await app.use(Plaintext).ensure(event)) ?? fallback(pubkey, message)
|
||||
|
||||
return {
|
||||
sign: (event, options) => signer.sign(event, options),
|
||||
getPubkey: () => signer.getPubkey(),
|
||||
nip04: {
|
||||
encrypt: (pubkey, message) => signer.nip04.encrypt(pubkey, message),
|
||||
decrypt: decryptVia((pubkey, message) => signer.nip04.decrypt(pubkey, message)),
|
||||
},
|
||||
nip44: {
|
||||
encrypt: (pubkey, message) => signer.nip44.encrypt(pubkey, message),
|
||||
decrypt: decryptVia((pubkey, message) => signer.nip44.decrypt(pubkey, message)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kind-10000 mute lists, keyed by pubkey. Mute lists carry private entries in
|
||||
* encrypted content, decoded through the plaintext cache (via a cache-backed
|
||||
* signer passed to the reader).
|
||||
*/
|
||||
export class MuteLists extends DerivedPlugin<MuteList> {
|
||||
constructor(app: IApp) {
|
||||
super(app, {
|
||||
filters: [{kinds: [MUTES]}],
|
||||
eventToItem: async (event: TrustedEvent) => {
|
||||
const content = await app.use(Plaintext).ensure(event)
|
||||
|
||||
return readList(asDecryptedEvent(event, {content}))
|
||||
},
|
||||
getKey: mute => mute.event.pubkey,
|
||||
eventToItem: event => MuteList.fromEvent(event, makeCachedSigner(app, event)),
|
||||
getKey: mute => mute.author(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,35 +59,26 @@ export class MuteLists extends DerivedPlugin<PublishedList> {
|
||||
return this.app.use(Network).loadUsingOutbox(pubkey, {kinds: [MUTES]}, relayHints)
|
||||
}
|
||||
|
||||
mutePublicly = async (tag: string[]) => {
|
||||
update = async (fn: (builder: MuteListBuilder) => void) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MUTES})
|
||||
const event = await addToListPublicly(list, tag).reconcile(user.nip44EncryptToSelf)
|
||||
const builder = new MuteListBuilder(await this.forceLoad(user.pubkey))
|
||||
|
||||
fn(builder)
|
||||
|
||||
const event = await builder.toTemplate(user.signer)
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
mutePrivately = async (tag: string[]) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MUTES})
|
||||
const event = await addToListPrivately(list, tag).reconcile(user.nip44EncryptToSelf)
|
||||
mutePublicly = (tag: string[]) => this.update(builder => builder.addPublic(tag))
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
mutePrivately = (tag: string[]) => this.update(builder => builder.addPrivate(tag))
|
||||
|
||||
unmute = async (value: string) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MUTES})
|
||||
const event = await removeFromList(list, value).reconcile(user.nip44EncryptToSelf)
|
||||
unmute = (value: string) => this.update(builder => builder.drop(nthEq(1, value)))
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
setMutes = async (updates: {publicTags?: string[][]; privateTags?: string[][]}) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: MUTES})
|
||||
const event = await updateList(list, updates).reconcile(user.nip44EncryptToSelf)
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
setMutes = (updates: {publicTags?: string[][]; privateTags?: string[][]}) =>
|
||||
this.update(builder => {
|
||||
if (updates.publicTags) builder.clearPublic().addPublic(...updates.publicTags)
|
||||
if (updates.privateTags) builder.clearPrivate().addPrivate(...updates.privateTags)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {chunk, first} from "@welshman/lib"
|
||||
import {RelayMode, getRelaysFromList, sortEventsDesc} from "@welshman/util"
|
||||
import {sortEventsDesc} from "@welshman/util"
|
||||
import type {Filter} from "@welshman/util"
|
||||
import {request, publish, diff, pull, push, makeLoader} from "@welshman/net"
|
||||
import type {
|
||||
@@ -44,7 +44,7 @@ export class Network {
|
||||
|
||||
loadUsingOutbox = async (pubkey: string, filter: Filter = {}, relayHints: string[] = []) => {
|
||||
const filters: Filter[] = [{...filter, authors: [pubkey]}]
|
||||
const writeRelays = getRelaysFromList(await this.app.use(RelayLists).load(pubkey), RelayMode.Write)
|
||||
const writeRelays = (await this.app.use(RelayLists).load(pubkey))?.writeUrls() ?? []
|
||||
const allRelays = this.app
|
||||
.use(Router)
|
||||
.FromRelays([...relayHints, ...writeRelays])
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import {
|
||||
PINS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
makeList,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {PINS} from "@welshman/util"
|
||||
import {PinList, PinListBuilder} from "@welshman/domain"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import {Network} from "./network.js"
|
||||
import {Thunks} from "./thunk.js"
|
||||
@@ -17,12 +10,12 @@ import type {IApp} from "../app.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 DerivedPlugin<ReturnType<typeof readList>> {
|
||||
export class PinLists extends DerivedPlugin<PinList> {
|
||||
constructor(app: IApp) {
|
||||
super(app, {
|
||||
filters: [{kinds: [PINS]}],
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
getKey: pins => pins.event.pubkey,
|
||||
eventToItem: PinList.factory(app.user?.signer),
|
||||
getKey: pins => pins.author(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,19 +23,18 @@ export class PinLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
return this.app.use(Network).loadUsingOutbox(pubkey, {kinds: [PINS]}, relayHints)
|
||||
}
|
||||
|
||||
pin = async (tag: string[]) => {
|
||||
update = async (fn: (builder: PinListBuilder) => void) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: PINS})
|
||||
const event = await addToListPublicly(list, tag).reconcile(user.nip44EncryptToSelf)
|
||||
const builder = new PinListBuilder(await this.forceLoad(user.pubkey))
|
||||
|
||||
fn(builder)
|
||||
|
||||
const event = await builder.toTemplate(user.signer)
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
unpin = async (value: string) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: PINS})
|
||||
const event = await removeFromList(list, value).reconcile(user.nip44EncryptToSelf)
|
||||
pin = (tag: string[]) => this.update(builder => builder.pinPublicly(tag))
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
unpin = (value: string) => this.update(builder => builder.unpin(value))
|
||||
}
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
import {derived, readable} from "svelte/store"
|
||||
import {
|
||||
readProfile,
|
||||
displayProfile,
|
||||
displayPubkey,
|
||||
isPublishedProfile,
|
||||
createProfile,
|
||||
editProfile,
|
||||
PROFILE,
|
||||
} from "@welshman/util"
|
||||
import type {Profile} from "@welshman/util"
|
||||
import {PROFILE} from "@welshman/util"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {Profile, ProfileBuilder, displayPubkey} from "@welshman/domain"
|
||||
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 {User} from "../user.js"
|
||||
import type {IApp} from "../app.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 DerivedPlugin<ReturnType<typeof readProfile>> {
|
||||
export class Profiles extends DerivedPlugin<Profile> {
|
||||
constructor(app: IApp) {
|
||||
super(app, {
|
||||
filters: [{kinds: [PROFILE]}],
|
||||
eventToItem: readProfile,
|
||||
getKey: profile => profile.event.pubkey,
|
||||
eventToItem: Profile.factory(app.user?.signer),
|
||||
getKey: profile => profile.author(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,17 +27,21 @@ export class Profiles extends DerivedPlugin<ReturnType<typeof readProfile>> {
|
||||
return this.app.use(Network).loadUsingOutbox(pubkey, {kinds: [PROFILE]}, relayHints)
|
||||
}
|
||||
|
||||
publish = (profile: Profile) => {
|
||||
// Publish the app user's kind-0, merging `values` over their current profile
|
||||
// (preserving any unknown metadata fields and source tags).
|
||||
publish = async (values: Record<string, any>) => {
|
||||
const user = User.require(this.app)
|
||||
const router = this.app.use(Router)
|
||||
const relays = router.merge([router.Index(), router.FromUser()]).getUrls()
|
||||
const event = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
|
||||
const builder = new ProfileBuilder(this.get(user.pubkey)).update(values)
|
||||
const event = await builder.toTemplate()
|
||||
|
||||
return this.app.use(Thunks).publish({event, relays})
|
||||
}
|
||||
|
||||
display = (pubkey: string | undefined, ...args: any[]): Projection<string> => {
|
||||
const read = ($profile: Maybe<ReturnType<typeof readProfile>>) =>
|
||||
pubkey ? displayProfile($profile, displayPubkey(pubkey)) : ""
|
||||
const read = ($profile: Maybe<Profile>) =>
|
||||
pubkey ? ($profile?.display() ?? displayPubkey(pubkey)) : ""
|
||||
|
||||
return projection(
|
||||
pubkey ? derived(this.one(pubkey, ...args), read) : readable(""),
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import {reject, nth, nthNe, nthEq, removeUndefined} from "@welshman/lib"
|
||||
import {
|
||||
RELAYS,
|
||||
RelayMode,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
getRelaysFromList,
|
||||
getRelayTags,
|
||||
getListTags,
|
||||
getRelayTagValues,
|
||||
makeList,
|
||||
makeEvent,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent, PublishedList} from "@welshman/util"
|
||||
import {RELAYS, RelayMode, getRelayTagValues} from "@welshman/util"
|
||||
import {RelayList, RelayListBuilder} from "@welshman/domain"
|
||||
import {DerivedPlugin} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
import {addMinimalFallbacks} from "@welshman/router"
|
||||
@@ -25,12 +13,12 @@ import type {IApp} from "../app.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 DerivedPlugin<PublishedList> {
|
||||
export class RelayLists extends DerivedPlugin<RelayList> {
|
||||
constructor(app: IApp) {
|
||||
super(app, {
|
||||
filters: [{kinds: [RELAYS]}],
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
getKey: (list: PublishedList) => list.event.pubkey,
|
||||
eventToItem: RelayList.factory(app.user?.signer),
|
||||
getKey: (list: RelayList) => list.author(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,40 +35,37 @@ export class RelayLists extends DerivedPlugin<PublishedList> {
|
||||
}
|
||||
|
||||
urls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list))
|
||||
this.project(pubkey, list => list?.urls() ?? [])
|
||||
|
||||
readUrls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list, RelayMode.Read))
|
||||
this.project(pubkey, list => list?.readUrls() ?? [])
|
||||
|
||||
writeUrls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list, RelayMode.Write))
|
||||
this.project(pubkey, list => list?.writeUrls() ?? [])
|
||||
|
||||
// NIP-65 relay-list mutations for the app's user
|
||||
|
||||
addRelay = async (url: string, mode: RelayMode) => {
|
||||
update = async (fn: (builder: RelayListBuilder) => void) => {
|
||||
const user = User.require(this.app)
|
||||
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}
|
||||
const builder = new RelayListBuilder(await this.forceLoad(user.pubkey))
|
||||
|
||||
fn(builder)
|
||||
|
||||
const event = await builder.toTemplate(user.signer)
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
addRelay = (url: string, mode: RelayMode) => this.update(builder => builder.addUrl(url, mode))
|
||||
|
||||
setReadRelays = (urls: string[]) => this.update(builder => builder.setReadUrls(urls))
|
||||
|
||||
setWriteRelays = (urls: string[]) => this.update(builder => builder.setWriteUrls(urls))
|
||||
|
||||
removeRelay = async (url: string, mode: RelayMode) => {
|
||||
const user = User.require(this.app)
|
||||
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}
|
||||
const builder = new RelayListBuilder(await this.forceLoad(user.pubkey))
|
||||
const event = await builder.removeUrl(url, mode).toTemplate(user.signer)
|
||||
|
||||
// publishToOutbox is outbox-only, so build relays here to also notify the
|
||||
// removed relay of its removal
|
||||
@@ -89,37 +74,15 @@ export class RelayLists extends DerivedPlugin<PublishedList> {
|
||||
return this.app.use(Thunks).publish({event, relays})
|
||||
}
|
||||
|
||||
setRelays = (tags: string[][]) => {
|
||||
setRelays = async (tags: string[][]) => {
|
||||
const user = User.require(this.app)
|
||||
const router = this.app.use(Router)
|
||||
const event = makeEvent(RELAYS, {tags})
|
||||
const builder = new RelayListBuilder(await this.forceLoad(user.pubkey))
|
||||
const event = await builder.setTags(tags).toTemplate(user.signer)
|
||||
const relays = router
|
||||
.merge([router.Index(), router.FromRelays(getRelayTagValues(tags))])
|
||||
.getUrls()
|
||||
|
||||
return this.app.use(Thunks).publish({event, relays})
|
||||
}
|
||||
|
||||
setReadRelays = async (urls: string[]) => {
|
||||
const user = User.require(this.app)
|
||||
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.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
setWriteRelays = async (urls: string[]) => {
|
||||
const user = User.require(this.app)
|
||||
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.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import {
|
||||
makeRoomCreateEvent,
|
||||
makeRoomDeleteEvent,
|
||||
makeRoomEditEvent,
|
||||
makeRoomJoinEvent,
|
||||
makeRoomLeaveEvent,
|
||||
makeRoomAddMemberEvent,
|
||||
makeRoomRemoveMemberEvent,
|
||||
} from "@welshman/util"
|
||||
import type {RoomMeta} from "@welshman/util"
|
||||
RoomCreateBuilder,
|
||||
RoomDeleteBuilder,
|
||||
RoomEditBuilder,
|
||||
RoomJoinBuilder,
|
||||
RoomLeaveBuilder,
|
||||
RoomAddMemberBuilder,
|
||||
RoomRemoveMemberBuilder,
|
||||
} from "@welshman/domain"
|
||||
import {Thunks} from "./thunk.js"
|
||||
import type {ThunkOptions} from "./thunk.js"
|
||||
import type {IApp} from "../app.js"
|
||||
|
||||
// Room metadata used when publishing NIP-29 room events. `h` is the group id.
|
||||
export type RoomMeta = {
|
||||
h: string
|
||||
name?: string
|
||||
about?: string
|
||||
picture?: string
|
||||
pictureMeta?: string[]
|
||||
isClosed?: boolean
|
||||
isHidden?: boolean
|
||||
isPrivate?: boolean
|
||||
isRestricted?: boolean
|
||||
livekit?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* NIP-29 relay-based group (room) management. Each method publishes the relevant
|
||||
* room event to the given relay as the app's user.
|
||||
@@ -22,19 +35,41 @@ export class Rooms {
|
||||
private publish = (url: string, event: ThunkOptions["event"]) =>
|
||||
this.app.use(Thunks).publish({event, relays: [url]})
|
||||
|
||||
create = (url: string, room: RoomMeta) => this.publish(url, makeRoomCreateEvent(room))
|
||||
create = async (url: string, room: RoomMeta) =>
|
||||
this.publish(url, await new RoomCreateBuilder().setGroup(room.h).toTemplate())
|
||||
|
||||
delete = (url: string, room: RoomMeta) => this.publish(url, makeRoomDeleteEvent(room))
|
||||
delete = async (url: string, room: RoomMeta) =>
|
||||
this.publish(url, await new RoomDeleteBuilder().setGroup(room.h).toTemplate())
|
||||
|
||||
edit = (url: string, room: RoomMeta) => this.publish(url, makeRoomEditEvent(room))
|
||||
edit = async (url: string, room: RoomMeta) => {
|
||||
const builder = new RoomEditBuilder().setGroup(room.h)
|
||||
|
||||
join = (url: string, room: RoomMeta) => this.publish(url, makeRoomJoinEvent(room))
|
||||
if (room.name) builder.setName(room.name)
|
||||
if (room.about) builder.setAbout(room.about)
|
||||
if (room.picture) builder.setPicture(room.picture, room.pictureMeta)
|
||||
|
||||
leave = (url: string, room: RoomMeta) => this.publish(url, makeRoomLeaveEvent(room))
|
||||
builder
|
||||
.setClosed(Boolean(room.isClosed))
|
||||
.setHidden(Boolean(room.isHidden))
|
||||
.setPrivate(Boolean(room.isPrivate))
|
||||
.setRestricted(Boolean(room.isRestricted))
|
||||
.setLivekit(Boolean(room.livekit))
|
||||
|
||||
addMember = (url: string, room: RoomMeta, pubkey: string) =>
|
||||
this.publish(url, makeRoomAddMemberEvent(room, pubkey))
|
||||
return this.publish(url, await builder.toTemplate())
|
||||
}
|
||||
|
||||
removeMember = (url: string, room: RoomMeta, pubkey: string) =>
|
||||
this.publish(url, makeRoomRemoveMemberEvent(room, pubkey))
|
||||
join = async (url: string, room: RoomMeta) =>
|
||||
this.publish(url, await new RoomJoinBuilder().setGroup(room.h).toTemplate())
|
||||
|
||||
leave = async (url: string, room: RoomMeta) =>
|
||||
this.publish(url, await new RoomLeaveBuilder().setGroup(room.h).toTemplate())
|
||||
|
||||
addMember = async (url: string, room: RoomMeta, pubkey: string) =>
|
||||
this.publish(url, await new RoomAddMemberBuilder().setGroup(room.h).addPubkey(pubkey).toTemplate())
|
||||
|
||||
removeMember = async (url: string, room: RoomMeta, pubkey: string) =>
|
||||
this.publish(
|
||||
url,
|
||||
await new RoomRemoveMemberBuilder().setGroup(room.h).addPubkey(pubkey).toTemplate(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import {derived} from "svelte/store"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {dec, inc, sortBy} from "@welshman/lib"
|
||||
import {PROFILE} from "@welshman/util"
|
||||
import type {PublishedProfile, RelayProfile} from "@welshman/util"
|
||||
import type {RelayProfile} from "@welshman/util"
|
||||
import type {Profile} from "@welshman/domain"
|
||||
import {throttled} from "@welshman/store"
|
||||
import type {IApp} from "../app.js"
|
||||
import {Network} from "./network.js"
|
||||
@@ -63,26 +64,19 @@ export const createSearch = <V, T>(options: T[], opts: SearchOptions<V, T>): Sea
|
||||
* fires a debounced NIP-50 network search through the app's loader.
|
||||
*/
|
||||
export class Searches {
|
||||
profileSearch: Readable<Search<string, PublishedProfile>>
|
||||
profileSearch: Readable<Search<string, Profile>>
|
||||
topicSearch: Readable<Search<string, Topic>>
|
||||
relaySearch: Readable<Search<string, RelayProfile>>
|
||||
|
||||
constructor(readonly app: IApp) {
|
||||
this.profileSearch = derived(
|
||||
[throttled(800, this.app.use(Profiles).all.$), throttled(800, this.app.use(Handles).index.$)],
|
||||
([$profiles, $handlesByNip05]) => {
|
||||
// Remove invalid nip05's from profiles
|
||||
const options = $profiles.map(p => {
|
||||
const isNip05Valid = !p.nip05 || $handlesByNip05.get(p.nip05)?.pubkey === p.event.pubkey
|
||||
|
||||
return isNip05Valid ? p : {...p, nip05: ""}
|
||||
})
|
||||
|
||||
return createSearch(options, {
|
||||
([$profiles, $handlesByNip05]) =>
|
||||
createSearch($profiles, {
|
||||
onSearch: this.searchProfiles,
|
||||
getValue: (profile: PublishedProfile) => profile.event.pubkey,
|
||||
getValue: (profile: Profile) => profile.author(),
|
||||
sortFn: ({score = 1, item}) => {
|
||||
const wotScore = this.app.use(Wot).graph.get().get(item.event.pubkey) || 0
|
||||
const wotScore = this.app.use(Wot).graph.get().get(item.author()) || 0
|
||||
|
||||
return dec(score) * inc(wotScore / (this.app.use(Wot).max.get() || 1))
|
||||
},
|
||||
@@ -95,9 +89,23 @@ export class Searches {
|
||||
],
|
||||
threshold: 0.3,
|
||||
shouldSort: false,
|
||||
// Read fields off the domain reader's parsed `values`; only expose a
|
||||
// nip05 that's verified against the loaded handle (anti-spoofing).
|
||||
getFn: (profile: Profile, path) => {
|
||||
const key = Array.isArray(path) ? path[0] : path
|
||||
|
||||
if (key === "nip05") {
|
||||
const nip05 = profile.nip05()
|
||||
|
||||
return nip05 && $handlesByNip05.get(nip05)?.pubkey === profile.author()
|
||||
? nip05
|
||||
: ""
|
||||
}
|
||||
|
||||
return profile.values[key] ?? ""
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
this.topicSearch = derived(this.app.use(Topics).all, $topics =>
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import {
|
||||
SEARCH_RELAYS,
|
||||
asDecryptedEvent,
|
||||
readList,
|
||||
getRelaysFromList,
|
||||
makeList,
|
||||
makeEvent,
|
||||
addToListPublicly,
|
||||
removeFromList,
|
||||
} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {SEARCH_RELAYS} from "@welshman/util"
|
||||
import {SearchRelayList, SearchRelayListBuilder} from "@welshman/domain"
|
||||
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 {Thunks} from "./thunk.js"
|
||||
import type {IApp} from "../app.js"
|
||||
@@ -22,12 +12,12 @@ import type {IApp} from "../app.js"
|
||||
* outbox model (the author's write relays), so it depends on the relay-list
|
||||
* collection.
|
||||
*/
|
||||
export class SearchRelayLists extends DerivedPlugin<ReturnType<typeof readList>> {
|
||||
export class SearchRelayLists extends DerivedPlugin<SearchRelayList> {
|
||||
constructor(app: IApp) {
|
||||
super(app, {
|
||||
filters: [{kinds: [SEARCH_RELAYS]}],
|
||||
eventToItem: (event: TrustedEvent) => readList(asDecryptedEvent(event)),
|
||||
getKey: searchRelayList => searchRelayList.event.pubkey,
|
||||
eventToItem: SearchRelayList.factory(app.user?.signer),
|
||||
getKey: list => list.author(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,27 +26,22 @@ export class SearchRelayLists extends DerivedPlugin<ReturnType<typeof readList>>
|
||||
}
|
||||
|
||||
urls = (pubkey: string): Projection<string[]> =>
|
||||
this.project(pubkey, list => getRelaysFromList(list))
|
||||
this.project(pubkey, list => list?.urls() ?? [])
|
||||
|
||||
addRelay = async (url: string) => {
|
||||
update = async (fn: (builder: SearchRelayListBuilder) => void) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: SEARCH_RELAYS})
|
||||
const event = await addToListPublicly(list, ["relay", url]).reconcile(user.nip44EncryptToSelf)
|
||||
const builder = new SearchRelayListBuilder(await this.forceLoad(user.pubkey))
|
||||
|
||||
fn(builder)
|
||||
|
||||
const event = await builder.toTemplate(user.signer)
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
|
||||
removeRelay = async (url: string) => {
|
||||
const user = User.require(this.app)
|
||||
const list = (await this.forceLoad(user.pubkey)) || makeList({kind: SEARCH_RELAYS})
|
||||
const event = await removeFromList(list, url).reconcile(user.nip44EncryptToSelf)
|
||||
addUrl = (url: string) => this.update(builder => builder.addUrl(url))
|
||||
|
||||
return this.app.use(Thunks).publishToOutbox({event})
|
||||
}
|
||||
removeUrl = (url: string) => this.update(builder => builder.removeUrl(url))
|
||||
|
||||
setRelays = (urls: string[]) =>
|
||||
this.app.use(Thunks).publish({
|
||||
event: makeEvent(SEARCH_RELAYS, {tags: urls.map(url => ["relay", url])}),
|
||||
relays: this.app.use(Router).FromUser().getUrls(),
|
||||
})
|
||||
setUrls = (urls: string[]) => this.update(builder => builder.setUrls(urls))
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
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 {FollowList, MuteList} from "@welshman/domain"
|
||||
import type {IApp} from "../app.js"
|
||||
import {projection, projectFrom} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
import {FollowLists} from "./follows.js"
|
||||
import {MuteLists} from "./mutes.js"
|
||||
|
||||
const listPubkeys = (list: List | undefined) => getPubkeyTagValues(getListTags(list))
|
||||
|
||||
/**
|
||||
* Web-of-trust scoring derived from follow and mute lists. The trust graph is
|
||||
* built from the perspective of the app's user (or, with no user, the union
|
||||
@@ -32,8 +29,8 @@ export class Wot {
|
||||
const $followersByPubkey = new Map<string, Set<string>>()
|
||||
|
||||
for (const list of lists.values()) {
|
||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||
addToMapKey($followersByPubkey, pubkey, list.event.pubkey)
|
||||
for (const pubkey of list?.pubkeys() ?? []) {
|
||||
addToMapKey($followersByPubkey, pubkey, list.author())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +45,8 @@ export class Wot {
|
||||
const $mutersByPubkey = new Map<string, Set<string>>()
|
||||
|
||||
for (const list of lists.values()) {
|
||||
for (const pubkey of getPubkeyTagValues(getListTags(list))) {
|
||||
addToMapKey($mutersByPubkey, pubkey, list.event.pubkey)
|
||||
for (const pubkey of list?.pubkeys() ?? []) {
|
||||
addToMapKey($mutersByPubkey, pubkey, list.author())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +61,16 @@ export class Wot {
|
||||
const $muteLists = this.app.use(MuteLists).index.get()
|
||||
const $pubkey = this.app.user?.pubkey
|
||||
const $graph = new Map<string, number>()
|
||||
const roots = $pubkey ? listPubkeys($followLists.get($pubkey)) : Array.from($followLists.keys())
|
||||
const roots = $pubkey
|
||||
? ($followLists.get($pubkey)?.pubkeys() ?? [])
|
||||
: Array.from($followLists.keys())
|
||||
|
||||
for (const follow of roots) {
|
||||
for (const pubkey of listPubkeys($followLists.get(follow))) {
|
||||
for (const pubkey of $followLists.get(follow)?.pubkeys() ?? []) {
|
||||
$graph.set(pubkey, inc($graph.get(pubkey)))
|
||||
}
|
||||
|
||||
for (const pubkey of listPubkeys($muteLists.get(follow))) {
|
||||
for (const pubkey of $muteLists.get(follow)?.pubkeys() ?? []) {
|
||||
$graph.set(pubkey, dec($graph.get(pubkey)))
|
||||
}
|
||||
}
|
||||
@@ -96,18 +95,18 @@ export class Wot {
|
||||
}
|
||||
|
||||
follows = (pubkey: string): Projection<string[]> =>
|
||||
projectFrom(this.app.use(FollowLists).index, $lists => listPubkeys($lists.get(pubkey)))
|
||||
projectFrom(this.app.use(FollowLists).index, $lists => $lists.get(pubkey)?.pubkeys() ?? [])
|
||||
|
||||
mutes = (pubkey: string): Projection<string[]> =>
|
||||
projectFrom(this.app.use(MuteLists).index, $lists => listPubkeys($lists.get(pubkey)))
|
||||
projectFrom(this.app.use(MuteLists).index, $lists => $lists.get(pubkey)?.pubkeys() ?? [])
|
||||
|
||||
network = (pubkey: string): Projection<string[]> =>
|
||||
projectFrom(this.app.use(FollowLists).index, $lists => {
|
||||
const pubkeys = new Set(listPubkeys($lists.get(pubkey)))
|
||||
const pubkeys = new Set($lists.get(pubkey)?.pubkeys() ?? [])
|
||||
const network = new Set<string>()
|
||||
|
||||
for (const follow of pubkeys) {
|
||||
for (const tpk of listPubkeys($lists.get(follow))) {
|
||||
for (const tpk of $lists.get(follow)?.pubkeys() ?? []) {
|
||||
if (!pubkeys.has(tpk)) {
|
||||
network.add(tpk)
|
||||
}
|
||||
@@ -125,15 +124,18 @@ export class Wot {
|
||||
|
||||
followsWhoFollow = (pubkey: string, target: string): Projection<string[]> =>
|
||||
projectFrom(this.app.use(FollowLists).index, $lists =>
|
||||
listPubkeys($lists.get(pubkey)).filter(other =>
|
||||
listPubkeys($lists.get(other)).includes(target),
|
||||
($lists.get(pubkey)?.pubkeys() ?? []).filter(other =>
|
||||
($lists.get(other)?.pubkeys() ?? []).includes(target),
|
||||
),
|
||||
)
|
||||
|
||||
followsWhoMute = (pubkey: string, target: string): Projection<string[]> => {
|
||||
const read = ($follows: ReadonlyMap<string, List>, $mutes: ReadonlyMap<string, List>) =>
|
||||
listPubkeys($follows.get(pubkey)).filter(other =>
|
||||
listPubkeys($mutes.get(other)).includes(target),
|
||||
const read = (
|
||||
$follows: ReadonlyMap<string, FollowList>,
|
||||
$mutes: ReadonlyMap<string, MuteList>,
|
||||
) =>
|
||||
($follows.get(pubkey)?.pubkeys() ?? []).filter(other =>
|
||||
($mutes.get(other)?.pubkeys() ?? []).includes(target),
|
||||
)
|
||||
|
||||
return projection(
|
||||
@@ -147,8 +149,8 @@ export class Wot {
|
||||
|
||||
wotScore = (pubkey: string, target: string): Projection<number> => {
|
||||
const read = (
|
||||
$follows: ReadonlyMap<string, List>,
|
||||
$mutes: ReadonlyMap<string, List>,
|
||||
$follows: ReadonlyMap<string, FollowList>,
|
||||
$mutes: ReadonlyMap<string, MuteList>,
|
||||
$followers: ReadonlyMap<string, Set<string>>,
|
||||
$muters: ReadonlyMap<string, Set<string>>,
|
||||
) => {
|
||||
@@ -156,10 +158,10 @@ export class Wot {
|
||||
let mutes: string[]
|
||||
|
||||
if (pubkey) {
|
||||
const theirFollows = listPubkeys($follows.get(pubkey))
|
||||
const theirFollows = $follows.get(pubkey)?.pubkeys() ?? []
|
||||
|
||||
follows = theirFollows.filter(other => listPubkeys($follows.get(other)).includes(target))
|
||||
mutes = theirFollows.filter(other => listPubkeys($mutes.get(other)).includes(target))
|
||||
follows = theirFollows.filter(other => ($follows.get(other)?.pubkeys() ?? []).includes(target))
|
||||
mutes = theirFollows.filter(other => ($mutes.get(other)?.pubkeys() ?? []).includes(target))
|
||||
} else {
|
||||
follows = Array.from($followers.get(target) || [])
|
||||
mutes = Array.from($muters.get(target) || [])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {get, writable} from "svelte/store"
|
||||
import {TaskQueue, uniq, now} from "@welshman/lib"
|
||||
import {getPubkeyTagValues, getRelaysFromList, prep} from "@welshman/util"
|
||||
import {getPubkeyTagValues, prep} from "@welshman/util"
|
||||
import type {TrustedEvent, SignedEvent, EventTemplate} from "@welshman/util"
|
||||
import {Nip59} from "@welshman/signer"
|
||||
import {MergedThunk, Thunks} from "./thunk.js"
|
||||
@@ -70,7 +70,7 @@ export class Wraps {
|
||||
return new MergedThunk(
|
||||
await Promise.all(
|
||||
uniq(recipients).map(async recipient => {
|
||||
const relays = getRelaysFromList(await this.app.use(MessagingRelayLists).load(recipient))
|
||||
const relays = (await this.app.use(MessagingRelayLists).load(recipient))?.urls() ?? []
|
||||
|
||||
return this.app.use(Thunks).publish({event: stableEvent, relays, recipient, ...options})
|
||||
}),
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {getTagValue, getZapSplits, zapFromEvent} from "@welshman/util"
|
||||
import type {Zapper, Zap, TrustedEvent} from "@welshman/util"
|
||||
import type {Profile} from "@welshman/domain"
|
||||
import {deriveDeduplicated, deriveDeduplicatedByValue} from "@welshman/store"
|
||||
import {LoadableMapPlugin, projection} from "./base.js"
|
||||
import type {Projection} from "./base.js"
|
||||
@@ -66,14 +67,19 @@ export class Zappers extends LoadableMapPlugin<Zapper> {
|
||||
loadForPubkey = async (pubkey: string, relays: string[] = []) => {
|
||||
const $profile = await this.app.use(Profiles).load(pubkey, relays)
|
||||
|
||||
return $profile?.lnurl ? this.load($profile.lnurl) : undefined
|
||||
const lnurl = $profile?.lnurl()
|
||||
|
||||
return lnurl ? this.load(lnurl) : undefined
|
||||
}
|
||||
|
||||
forPubkey = (pubkey: string, relays: string[] = []): Projection<Maybe<Zapper>> => {
|
||||
this.loadForPubkey(pubkey, relays)
|
||||
|
||||
const read = ([$zappersByLnurl, $profile]: [ReadonlyMap<string, Zapper>, Maybe<{lnurl?: string}>]) =>
|
||||
$profile?.lnurl ? $zappersByLnurl.get($profile.lnurl) : undefined
|
||||
const read = ([$zappersByLnurl, $profile]: [ReadonlyMap<string, Zapper>, Maybe<Profile>]) => {
|
||||
const lnurl = $profile?.lnurl()
|
||||
|
||||
return lnurl ? $zappersByLnurl.get(lnurl) : undefined
|
||||
}
|
||||
|
||||
return projection(
|
||||
deriveDeduplicated([this.index.$, this.app.use(Profiles).one(pubkey, relays)], read),
|
||||
@@ -120,12 +126,12 @@ export class Zappers extends LoadableMapPlugin<Zapper> {
|
||||
|
||||
const read = (values: any[]) => {
|
||||
const $zappersByLnurl = values[0] as Map<string, Zapper>
|
||||
const $profiles = values.slice(1) as Array<{lnurl?: string} | undefined>
|
||||
const $profiles = values.slice(1) as Array<Profile | undefined>
|
||||
|
||||
const zapperByPubkey = new Map<string, Zapper>()
|
||||
|
||||
splits.forEach((split, i) => {
|
||||
const lnurl = $profiles[i]?.lnurl
|
||||
const lnurl = $profiles[i]?.lnurl()
|
||||
const zapper = lnurl ? $zappersByLnurl.get(lnurl) : undefined
|
||||
|
||||
if (zapper) zapperByPubkey.set(split.pubkey, zapper)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"@welshman/domain": ["../domain/src/index.js"],
|
||||
"@welshman/feeds": ["../feeds/src/index.js"],
|
||||
"@welshman/lib": ["../lib/src/index.js"],
|
||||
"@welshman/net": ["../net/src/index.js"],
|
||||
|
||||
Reference in New Issue
Block a user