Small fixes, rework zaps
This commit is contained in:
@@ -1,51 +1,11 @@
|
||||
import {tryCatch, fetchJson, batcher, postJson, last} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {tryCatch, batcher, postJson} from "@welshman/lib"
|
||||
import {queryProfile, displayNip05} from "@welshman/util"
|
||||
import type {Handle} from "@welshman/util"
|
||||
import {deriveDeduplicated} from "@welshman/store"
|
||||
import {LoadableData} from "./clientData.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import {Profiles} from "./profiles.js"
|
||||
|
||||
export type Handle = {
|
||||
nip05: string
|
||||
pubkey?: string
|
||||
nip46?: string[]
|
||||
relays?: string[]
|
||||
}
|
||||
|
||||
export async function queryProfile(nip05: string): Promise<Maybe<Handle>> {
|
||||
const parts = nip05.split("@")
|
||||
const name = parts.length > 1 ? parts[0] : "_"
|
||||
const domain = last(parts)
|
||||
|
||||
try {
|
||||
const {
|
||||
names,
|
||||
relays = {},
|
||||
nip46 = {},
|
||||
} = await fetchJson(`https://${domain}/.well-known/nostr.json?name=${name}`)
|
||||
|
||||
const pubkey = names[name]
|
||||
|
||||
if (!pubkey) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
nip05,
|
||||
pubkey,
|
||||
nip46: nip46[pubkey],
|
||||
relays: relays[pubkey],
|
||||
}
|
||||
} catch (_e) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const displayNip05 = (nip05: string) =>
|
||||
nip05?.startsWith("_@") ? last(nip05.split("@")) : nip05
|
||||
|
||||
export const displayHandle = (handle: Handle) => displayNip05(handle.nip05)
|
||||
|
||||
/**
|
||||
* NIP-05 handles, keyed by nip05 identifier. A "local" loadable collection:
|
||||
* items aren't nostr events, they're fetched over HTTP (either directly from
|
||||
|
||||
@@ -27,16 +27,6 @@ export class MuteLists extends RepositoryCollection<PublishedList> {
|
||||
eventToItem: async (event: TrustedEvent) => {
|
||||
const content = await ctx.use(Plaintext).ensure(event)
|
||||
|
||||
// If this is our own mute list but it couldn't be decrypted yet because
|
||||
// no signer is available, don't cache a result with empty private tags —
|
||||
// that would get stuck permanently since the repository view won't
|
||||
// re-process an already-seen event id. Returning undefined leaves it
|
||||
// uncached so it's retried once a signer is available. For other
|
||||
// pubkeys' lists we fall through and read just the public tags.
|
||||
if (event.content && content === undefined && event.pubkey === ctx.user?.pubkey) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return readList(asDecryptedEvent(event, {content}))
|
||||
},
|
||||
getKey: mute => mute.event.pubkey,
|
||||
|
||||
@@ -5,38 +5,23 @@ import {ClientData} from "./clientData.js"
|
||||
|
||||
/**
|
||||
* A cache of decrypted event content, keyed by event id.
|
||||
*
|
||||
* In the old global model decryption used `getSigner(getSession(event.pubkey))`
|
||||
* — whichever logged-in account authored the event. In the per-client model
|
||||
* there is exactly one identity, so this reduces to "is this our user?". That
|
||||
* scoping is also what keeps decrypted content (including DM rumors) from
|
||||
* bleeding across identities — each client decrypts only its own.
|
||||
*/
|
||||
export class Plaintext extends ClientData<string> {
|
||||
ensure = async (event: TrustedEvent): Promise<Maybe<string>> => {
|
||||
// Check for key presence rather than truthiness so a legitimately empty
|
||||
// decrypted result ("") is treated as cached and we don't re-hit the signer
|
||||
// on every call.
|
||||
if (event.content && this.get(event.id) === undefined) {
|
||||
const signer = event.pubkey === this.ctx.user?.pubkey ? this.ctx.user?.signer : undefined
|
||||
|
||||
if (!signer) return
|
||||
|
||||
let result
|
||||
if (this.ctx.user?.pubkey !== event.pubkey) return
|
||||
|
||||
let result = this.get(event.id)
|
||||
if (event.content && result === undefined) {
|
||||
try {
|
||||
result = await decrypt(signer, event.pubkey, event.content)
|
||||
result = await decrypt(this.ctx.user.signer, event.pubkey, event.content)
|
||||
this.set(event.id, result)
|
||||
} catch (e: any) {
|
||||
if (!String(e).match(/invalid base64/)) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
if (result !== undefined) {
|
||||
this.set(event.id, result)
|
||||
}
|
||||
}
|
||||
|
||||
return this.get(event.id)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,22 +93,10 @@ export const clientPolicyIngest: ClientPolicy = client =>
|
||||
})
|
||||
|
||||
/**
|
||||
* Wires socket activity on the client's pool into the RelayStats store.
|
||||
* Listens to socket activity on the client's pool into the RelayStats store.
|
||||
*/
|
||||
export const clientPolicyRelayStats: ClientPolicy = client => {
|
||||
const stats = client.use(RelayStats)
|
||||
|
||||
return client.pool.subscribe(socket => {
|
||||
socket.on(SocketEvent.Send, stats.onSocketSend)
|
||||
socket.on(SocketEvent.Receive, stats.onSocketReceive)
|
||||
socket.on(SocketEvent.Status, stats.onSocketStatus)
|
||||
|
||||
return () => {
|
||||
socket.off(SocketEvent.Send, stats.onSocketSend)
|
||||
socket.off(SocketEvent.Receive, stats.onSocketReceive)
|
||||
socket.off(SocketEvent.Status, stats.onSocketStatus)
|
||||
}
|
||||
})
|
||||
return client.pool.subscribe(client.use(RelayStats).monitorSocket)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {groupBy, batch, now, uniq, ago, DAY, HOUR, MINUTE} from "@welshman/lib"
|
||||
import {isOnionUrl, isLocalUrl, isIPAddress, isRelayUrl} from "@welshman/util"
|
||||
import {SocketStatus} from "@welshman/net"
|
||||
import type {ClientMessage, RelayMessage} from "@welshman/net"
|
||||
import {SocketStatus, SocketEvent} from "@welshman/net"
|
||||
import type {ClientMessage, RelayMessage, Socket} from "@welshman/net"
|
||||
import {ClientData} from "./clientData.js"
|
||||
import {BlockedRelayLists} from "./blockedRelayLists.js"
|
||||
|
||||
@@ -110,7 +110,7 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
||||
}
|
||||
})
|
||||
|
||||
onSocketSend = ([verb]: ClientMessage, url: string) => {
|
||||
private onSocketSend = ([verb]: ClientMessage, url: string) => {
|
||||
if (verb === "REQ") {
|
||||
this.update([
|
||||
url,
|
||||
@@ -130,7 +130,7 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
||||
}
|
||||
}
|
||||
|
||||
onSocketReceive = ([verb, ...extra]: RelayMessage, url: string) => {
|
||||
private onSocketReceive = ([verb, ...extra]: RelayMessage, url: string) => {
|
||||
if (verb === "OK") {
|
||||
const [, ok] = extra
|
||||
|
||||
@@ -161,7 +161,7 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
||||
}
|
||||
}
|
||||
|
||||
onSocketStatus = (status: string, url: string) => {
|
||||
private onSocketStatus = (status: string, url: string) => {
|
||||
if (status === SocketStatus.Open) {
|
||||
this.update([
|
||||
url,
|
||||
@@ -192,4 +192,16 @@ export class RelayStats extends ClientData<RelayStatsItem> {
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
monitorSocket = (socket: Socket) => {
|
||||
socket.on(SocketEvent.Send, this.onSocketSend)
|
||||
socket.on(SocketEvent.Receive, this.onSocketReceive)
|
||||
socket.on(SocketEvent.Status, this.onSocketStatus)
|
||||
|
||||
return () => {
|
||||
socket.off(SocketEvent.Send, this.onSocketSend)
|
||||
socket.off(SocketEvent.Receive, this.onSocketReceive)
|
||||
socket.off(SocketEvent.Status, this.onSocketStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {writable} from "svelte/store"
|
||||
import type {Readable} from "svelte/store"
|
||||
import {
|
||||
removeUndefined,
|
||||
fetchJson,
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
batcher,
|
||||
postJson,
|
||||
} from "@welshman/lib"
|
||||
import {getTagValues, zapFromEvent} from "@welshman/util"
|
||||
import {getTagValue, getZapSplits, zapFromEvent} from "@welshman/util"
|
||||
import type {Zapper, Zap, TrustedEvent} from "@welshman/util"
|
||||
import {deriveDeduplicated} from "@welshman/store"
|
||||
import {deriveDeduplicated, deriveDeduplicatedByValue} from "@welshman/store"
|
||||
import {LoadableData} from "./clientData.js"
|
||||
import type {IClient} from "./client.js"
|
||||
import {Profiles} from "./profiles.js"
|
||||
@@ -22,10 +22,6 @@ import {Profiles} from "./profiles.js"
|
||||
* profiles collection to resolve a pubkey's lnurl.
|
||||
*/
|
||||
export class Zappers extends LoadableData<Zapper> {
|
||||
constructor(ctx: IClient) {
|
||||
super(ctx)
|
||||
}
|
||||
|
||||
fetch = batcher(800, async (lnurls: string[]) => {
|
||||
const result = new Map<string, Zapper>()
|
||||
const valid = lnurls.filter(lnurl => lnurl.startsWith("lnurl1"))
|
||||
@@ -40,7 +36,6 @@ export class Zappers extends LoadableData<Zapper> {
|
||||
}
|
||||
}
|
||||
|
||||
// Use dufflepud if it's set up to protect user privacy, otherwise fetch directly
|
||||
if (this.ctx.config.dufflepudUrl) {
|
||||
const hexUrls = valid.map(bech32ToHex)
|
||||
const res: any = await tryCatch(
|
||||
@@ -82,45 +77,69 @@ export class Zappers extends LoadableData<Zapper> {
|
||||
)
|
||||
}
|
||||
|
||||
getLnUrlsForEvent = async (event: TrustedEvent) => {
|
||||
const pubkeys = getTagValues("zap", event.tags)
|
||||
/**
|
||||
* Resolve the zapper a zap receipt should be validated against. A receipt's
|
||||
* `p` tag is the recipient (copied from the zap request), so we honor only
|
||||
* receipts addressed to one of the parent's designated split recipients and
|
||||
* load *that* recipient's zapper. The old lookup always used the first
|
||||
* recipient's lnurl, which silently dropped legitimate zaps to any of the
|
||||
* other split recipients.
|
||||
*/
|
||||
loadZapperForZap = async (zapReceipt: TrustedEvent, parent: TrustedEvent) => {
|
||||
const recipient = getTagValue("p", zapReceipt.tags)
|
||||
const split = getZapSplits(parent).find(split => split.pubkey === recipient)
|
||||
|
||||
if (pubkeys.length > 0) {
|
||||
const profiles = await Promise.all(pubkeys.map(pubkey => this.ctx.use(Profiles).load(pubkey)))
|
||||
const lnurls = removeUndefined(profiles.map(profile => profile?.lnurl))
|
||||
if (!split) return
|
||||
|
||||
if (lnurls.length > 0) {
|
||||
return lnurls
|
||||
}
|
||||
return this.loadForPubkey(split.pubkey, removeUndefined([split.relay]))
|
||||
}
|
||||
|
||||
validateZapReceipt = async (zapReceipt: TrustedEvent, parent: TrustedEvent) => {
|
||||
const zapper = await this.loadZapperForZap(zapReceipt, parent)
|
||||
|
||||
return zapper ? zapFromEvent(zapReceipt, zapper) : undefined
|
||||
}
|
||||
|
||||
validateZapReceipts = async (zapReceipts: TrustedEvent[], parent: TrustedEvent) =>
|
||||
removeUndefined(
|
||||
await Promise.all(zapReceipts.map(zapReceipt => this.validateZapReceipt(zapReceipt, parent))),
|
||||
)
|
||||
|
||||
deriveValidZapReceipts = (zapReceipts: TrustedEvent[], parent: TrustedEvent): Readable<Zap[]> => {
|
||||
const splits = getZapSplits(parent)
|
||||
const profiles = this.ctx.use(Profiles)
|
||||
|
||||
// Ensure each recipient's profile (-> lnurl) and zapper are being loaded.
|
||||
for (const split of splits) {
|
||||
this.loadForPubkey(split.pubkey, removeUndefined([split.relay]))
|
||||
}
|
||||
|
||||
const profile = await this.ctx.use(Profiles).load(event.pubkey)
|
||||
const stores: Readable<any>[] = [
|
||||
this.index,
|
||||
...splits.map(split => profiles.derive(split.pubkey)),
|
||||
]
|
||||
|
||||
return removeUndefined([profile?.lnurl])
|
||||
}
|
||||
return deriveDeduplicatedByValue(stores, (values: any[]) => {
|
||||
const $zappersByLnurl = values[0] as Map<string, Zapper>
|
||||
const $profiles = values.slice(1) as Array<{lnurl?: string} | undefined>
|
||||
|
||||
getZapperForZap = async (zap: TrustedEvent, parent: TrustedEvent) => {
|
||||
const lnurls = await this.getLnUrlsForEvent(parent)
|
||||
const zapperByPubkey = new Map<string, Zapper>()
|
||||
|
||||
return lnurls.length > 0 ? this.load(lnurls[0]) : undefined
|
||||
}
|
||||
splits.forEach((split, i) => {
|
||||
const lnurl = $profiles[i]?.lnurl
|
||||
const zapper = lnurl ? $zappersByLnurl.get(lnurl) : undefined
|
||||
|
||||
getValidZap = async (zap: TrustedEvent, parent: TrustedEvent) => {
|
||||
const zapper = await this.getZapperForZap(zap, parent)
|
||||
if (zapper) zapperByPubkey.set(split.pubkey, zapper)
|
||||
})
|
||||
|
||||
return zapper ? zapFromEvent(zap, zapper) : undefined
|
||||
}
|
||||
return removeUndefined(
|
||||
zapReceipts.map(zapReceipt => {
|
||||
const recipient = getTagValue("p", zapReceipt.tags)
|
||||
const zapper = recipient ? zapperByPubkey.get(recipient) : undefined
|
||||
|
||||
getValidZaps = async (zaps: TrustedEvent[], parent: TrustedEvent) =>
|
||||
removeUndefined(await Promise.all(zaps.map(zap => this.getValidZap(zap, parent))))
|
||||
|
||||
deriveValidZaps = (zaps: TrustedEvent[], parent: TrustedEvent) => {
|
||||
const store = writable<Zap[]>([])
|
||||
|
||||
this.getValidZaps(zaps, parent).then(validZaps => {
|
||||
store.set(validZaps)
|
||||
return zapper ? zapFromEvent(zapReceipt, zapper) : undefined
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
return store
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user