From a2077e3d40802d656d4fd9ed126c6c497ba44b39 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:13:55 -0300 Subject: [PATCH] feat: hydrate zap receipt kind 9735 - gatherZapSender, gatherZapped --- src/storages/hydrate.ts | 74 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 7b11cfb8..17267391 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -1,12 +1,15 @@ import { NStore } from '@nostrify/nostrify'; import { Kysely } from 'kysely'; import { matchFilter } from 'nostr-tools'; +import { NSchema as n } from '@nostrify/nostrify'; +import { z } from 'zod'; import { DittoTables } from '@/db/DittoTables.ts'; import { Conf } from '@/config.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { findQuoteTag } from '@/utils/tags.ts'; import { findQuoteInContent } from '@/utils/note.ts'; +import { getAmount } from '@/utils/bolt11.ts'; import { Storages } from '@/storages.ts'; interface HydrateOpts { @@ -58,6 +61,14 @@ async function hydrateEvents(opts: HydrateOpts): Promise { cache.push(event); } + for (const event of await gatherZapped({ events: cache, store, signal })) { + cache.push(event); + } + + for (const event of await gatherZapSender({ events: cache, store, signal })) { + cache.push(event); + } + const stats = { authors: await gatherAuthorStats(cache, kysely as Kysely), events: await gatherEventStats(cache, kysely as Kysely), @@ -130,6 +141,27 @@ export function assembleEvents( event.reported_notes = reportedEvents; } + if (event.kind === 9735) { + const amountSchema = z.coerce.number().int().nonnegative().catch(0); + // amount in millisats + const amount = amountSchema.parse(getAmount(event?.tags.find(([name]) => name === 'bolt11')?.[1])); + event.zap_amount = amount; + + const id = event.tags.find(([name]) => name === 'e')?.[1]; + if (id) { + event.zapped = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e)); + } + + const zapRequestString = event?.tags?.find(([name]) => name === 'description')?.[1]; + const zapRequest = n.json().pipe(n.event()).optional().catch(undefined).parse(zapRequestString); + // By getting the pubkey from the zap request we guarantee who is the sender + // some clients don't put the P tag in the zap receipt... + const zapSender = zapRequest?.pubkey; + if (zapSender) { + event.zap_sender = b.find((e) => matchFilter({ kinds: [0], authors: [zapSender] }, e)) ?? zapSender; + } + } + event.author_stats = stats.authors.find((stats) => stats.pubkey === event.pubkey); event.event_stats = eventStats.find((stats) => stats.event_id === event.id); } @@ -277,6 +309,48 @@ function gatherReportedProfiles({ events, store, signal }: HydrateOpts): Promise ); } +/** Collect events being zapped. */ +function gatherZapped({ events, store, signal }: HydrateOpts): Promise { + const ids = new Set(); + + for (const event of events) { + if (event.kind === 9735) { + const id = event.tags.find(([name]) => name === 'e')?.[1]; + if (id) { + ids.add(id); + } + } + } + + return store.query( + [{ ids: [...ids], limit: ids.size }], + { signal }, + ); +} + +/** Collect author that zapped. */ +function gatherZapSender({ events, store, signal }: HydrateOpts): Promise { + const pubkeys = new Set(); + + for (const event of events) { + if (event.kind === 9735) { + const zapRequestString = event?.tags?.find(([name]) => name === 'description')?.[1]; + const zapRequest = n.json().pipe(n.event()).optional().catch(undefined).parse(zapRequestString); + // By getting the pubkey from the zap request we guarantee who is the sender + // some clients don't put the P tag in the zap receipt... + const zapSender = zapRequest?.pubkey; + if (zapSender) { + pubkeys.add(zapSender); + } + } + } + + return store.query( + [{ kinds: [0], limit: pubkeys.size }], + { signal }, + ); +} + /** Collect author stats from the events. */ async function gatherAuthorStats( events: DittoEvent[],