Files
welshman/packages/app/src/plugins/wraps.ts
T
Jon Staab 22a666d497
tests / tests (push) Failing after 5m17s
Format
2026-06-22 10:23:15 -07:00

80 lines
2.6 KiB
TypeScript

import {TaskQueue, uniq, now} from "@welshman/lib"
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"
import type {ThunkOptions} from "./thunk.js"
import {User} from "../user.js"
import {MessagingRelayLists} from "./messagingRelayLists.js"
import type {IApp} from "../app.js"
export type SendWrappedOptions = Omit<
ThunkOptions,
"event" | "relays" | "recipient" | "app" | "user"
> & {
event: EventTemplate
recipients: string[]
}
/**
* Per-app wrap (NIP-59) state: the unwrap queue plus failure/dedup
* tracking. Scoped to `app.user`, so an app 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
* `appPolicyWraps`.
*/
export class Wraps {
failedUnwraps = new Set<string>()
queue: TaskQueue<TrustedEvent>
constructor(readonly app: IApp) {
this.queue = new TaskQueue<TrustedEvent>({
batchSize: 50,
batchDelay: 30,
processItem: async (wrap: TrustedEvent) => {
const signer = this.app.user?.signer
const recipient = this.app.user?.pubkey
// Only unwrap messages addressed to our user
if (!signer || !recipient || !getPubkeyTagValues(wrap.tags).includes(recipient)) {
return
}
try {
const rumor = await Nip59.fromSigner(signer).unwrap(wrap as SignedEvent)
this.app.wrapManager.add({wrap: wrap as SignedEvent, rumor, recipient})
} catch (e) {
this.failedUnwraps.add(wrap.id)
}
},
})
}
enqueue = (wrap: TrustedEvent) => {
if (this.failedUnwraps.has(wrap.id)) return
if (this.app.wrapManager.getRumor(wrap.id)) return
this.queue.push(wrap)
}
// NIP-59: wrap an event for each recipient (using their messaging relays) and
// publish the wraps as the app's user.
publish = async ({event, recipients, ...options}: SendWrappedOptions) => {
const user = User.require(this.app)
// Stabilize the event id across the different wraps
const stableEvent = prep(event, user.pubkey, now())
return new MergedThunk(
await Promise.all(
uniq(recipients).map(async recipient => {
const relays = (await this.app.use(MessagingRelayLists).load(recipient))?.urls() ?? []
return this.app.use(Thunks).publish({event: stableEvent, relays, recipient, ...options})
}),
),
)
}
}