From cbc1691002e79cc260aa9cc27ddc8b32385dbcc3 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sun, 15 Sep 2024 18:34:11 -0300 Subject: [PATCH 01/13] feat: zap notification in streaming --- src/controllers/api/streaming.ts | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index cfa8c3c5..3f05669d 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -1,4 +1,5 @@ -import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; +import TTLCache from '@isaacs/ttlcache'; +import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { z } from 'zod'; @@ -10,9 +11,10 @@ import { getFeedPubkeys } from '@/queries.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; import { bech32ToPubkey, Time } from '@/utils.ts'; +import { getAmount } from '@/utils/bolt11.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts'; -import TTLCache from '@isaacs/ttlcache'; +import { RenderNotificationOpts } from '@/views/mastodon/notifications.ts'; const debug = Debug('ditto:streaming'); @@ -155,7 +157,28 @@ const streamingController: AppController = async (c) => { if (['user', 'user:notification'].includes(stream) && pubkey) { sub([{ '#p': [pubkey] }], async (event) => { if (event.pubkey === pubkey) return; // skip own events - const payload = await renderNotification(event, { viewerPubkey: pubkey }); + + const opts: RenderNotificationOpts = { viewerPubkey: pubkey }; + + 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; + + const amountSchema = z.coerce.number().int().nonnegative().catch(0); + // amount in millisats + const amount = amountSchema.parse(getAmount(event?.tags.find(([name]) => name === 'bolt11')?.[1])); + + opts['zap'] = { + zapSender, + amount, + message: zapRequest?.content, + }; + } + + const payload = await renderNotification(event, opts); if (payload) { return { event: 'notification', @@ -215,7 +238,7 @@ async function topicToFilter( // HACK: this puts the user's entire contacts list into RAM, // and then calls `matchFilters` over it. Refreshing the page // is required after following a new user. - return pubkey ? { kinds: [1, 6], authors: await getFeedPubkeys(pubkey) } : undefined; + return pubkey ? { kinds: [1, 6, 9735], authors: await getFeedPubkeys(pubkey) } : undefined; } } From 6e2508063b12e38206ee34c4519dfa7584873f8e Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:10:10 -0300 Subject: [PATCH 02/13] feat(DittoEvent): add zapped, zap_sender & zap_amount --- src/interfaces/DittoEvent.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index 2f2aef26..53c0d553 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -37,4 +37,9 @@ export interface DittoEvent extends NostrEvent { reported_notes?: DittoEvent[]; /** Admin event relationship. */ info?: DittoEvent; + /** Kind 1 being zapped */ + zapped?: DittoEvent; + /** Kind 0 or pubkey that zapped */ + zap_sender?: DittoEvent | string; + zap_amount?: number; } From a2077e3d40802d656d4fd9ed126c6c497ba44b39 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:13:55 -0300 Subject: [PATCH 03/13] 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[], From 516866a90592b5c7e1229f3c0f698b2674ab209a Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:15:05 -0300 Subject: [PATCH 04/13] test(fixtures): jack kind 0 --- fixtures/events/kind-0-jack.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 fixtures/events/kind-0-jack.json diff --git a/fixtures/events/kind-0-jack.json b/fixtures/events/kind-0-jack.json new file mode 100644 index 00000000..d422377e --- /dev/null +++ b/fixtures/events/kind-0-jack.json @@ -0,0 +1 @@ +{"kind":0,"id":"f7b1a3ca3fa77bffded2024568da939e8cd3ed2403004e1ecb56d556f299ad2a","pubkey":"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2","created_at":1715441226,"tags":[],"content":"{\"banner\":\"https:\\/\\/m.primal.net\\/IBZO.jpg\",\"website\":\"\",\"picture\":\"https:\\/\\/image.nostr.build\\/26867ce34e4b11f0a1d083114919a9f4eca699f3b007454c396ef48c43628315.jpg\",\"lud06\":\"\",\"display_name\":\"\",\"lud16\":\"jack@primal.net\",\"nip05\":\"\",\"name\":\"jack\",\"about\":\"bitcoin \u0026 chill\"}","sig":"9792ceb1e9c73a6c2140540ddbac4279361cae4cc41888019d9dd47d09c1e7cee55948f6e1af824fa0f856d892686352bc757ad157f766f0da656d5e80b38bc7"} From 5caa482806b4a9a87b9fedfe51188d563f20a141 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:15:21 -0300 Subject: [PATCH 05/13] test(fixtures): patrick kind 0 --- fixtures/events/kind-0-patrick.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 fixtures/events/kind-0-patrick.json diff --git a/fixtures/events/kind-0-patrick.json b/fixtures/events/kind-0-patrick.json new file mode 100644 index 00000000..db0defbe --- /dev/null +++ b/fixtures/events/kind-0-patrick.json @@ -0,0 +1 @@ +{"kind":0,"id":"34bc588a4ff5ca8570a1ad4114485239f83c135b09636dbc16df338f73079e42","pubkey":"47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4","created_at":1726076335,"tags":[],"content":"{\"about\":\"Coding with nature's inspiration, embracing solitude's wisdom. Team Soapbox.\",\"bot\":false,\"lud16\":\"patrickreiis@getalby.com\",\"name\":\"patrickReiis\",\"nip05\":\"patrick@patrickdosreis.com\",\"picture\":\"https://image.nostr.build/2177817a323ed8a58d508fb25160e1c2f38f60256125b764c82c988869916e84.jpg\",\"website\":\"https://patrickdosreis.com/\",\"pubkey\":\"47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4\",\"npub\":\"npub1gujeqakgt7fyp6zjggxhyy7ft623qtcaay5lkc8n8gkry4cvnrzqd3f67z\",\"created_at\":1717600965}","sig":"2780887e58d6e59cc9c03cca8a583bc121d2c74d98cc434d22e65c1f56da1bb09d79fc7cc3c4ee5b829773c17d6f482b114dc951c1683c3908cedff783d785ad"} From 10f30b366102d95a42c5265097ba1d892c05ac9f Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:15:43 -0300 Subject: [PATCH 06/13] test(fixtures): patrick kind 1, post zapped --- fixtures/events/kind-1-being-zapped.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 fixtures/events/kind-1-being-zapped.json diff --git a/fixtures/events/kind-1-being-zapped.json b/fixtures/events/kind-1-being-zapped.json new file mode 100644 index 00000000..00d33f06 --- /dev/null +++ b/fixtures/events/kind-1-being-zapped.json @@ -0,0 +1 @@ +{"kind":1,"id":"02e52f80e2e6a3ad0993e9c4a7b4e6afc79d067c6ff9c6df3fb2896342dee2df","pubkey":"47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4","created_at":1724609131,"tags":[["e","677c701036eae20632a7677ee6eece0c62e259d5c72864d78a3bbe419c0d2d2c","wss://gleasonator.dev/relay","root"],["e","677c701036eae20632a7677ee6eece0c62e259d5c72864d78a3bbe419c0d2d2c","wss://gleasonator.dev/relay","reply"],["p","82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2"]],"content":"Please I don't want to go back to the shoe factory","sig":"ce6ca329701eec5db0b182bd52c48777b9eccaac298180a6601d8c5156060d944768d71376e7d24c24cefb6619d1467f6a30e0ca574d68f748b38c784e4ced59"} From 4f0f1182b8177d9dc9bb1825b03416b44f2bf493 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:15:58 -0300 Subject: [PATCH 07/13] test(fixtures): jack zaps patrick --- fixtures/events/kind-9735-jack-zap-patrick.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 fixtures/events/kind-9735-jack-zap-patrick.json diff --git a/fixtures/events/kind-9735-jack-zap-patrick.json b/fixtures/events/kind-9735-jack-zap-patrick.json new file mode 100644 index 00000000..2abe9210 --- /dev/null +++ b/fixtures/events/kind-9735-jack-zap-patrick.json @@ -0,0 +1 @@ +{"kind":9735,"id":"a57d30d59e7442f9a2ad329400a6cbf29c2b34b1e69e4cdce8bc2fe751d9268f","pubkey":"79f00d3f5a19ec806189fcab03c1be4ff81d18ee4f653c88fac41fe03570f432","created_at":1724610766,"tags":[["p","47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4"],["e","02e52f80e2e6a3ad0993e9c4a7b4e6afc79d067c6ff9c6df3fb2896342dee2df"],["P","82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2"],["bolt11","lnbc52250n1pnvk7xvpp5l776w7354zz9mh7sf3dlq8znkfjhysse9dwda9c7se7jwpglng0qhp5jp5cqy7n7wz9jlvd0aa40ws0d3e78l4ug2pzfen2m56mwg0qahrscqzzsxqyz5vqsp5v30pn2u86h3mz69wlvmu9vam9wudlnt4fv9wcxn24s6vrkj842gq9qxpqysgqw9mfxpyce3fhfue8p88exx8g6gn5ut9c2tz8awnw377dmhqymszrsjg49waxprkd6ggdzn90dwpgjwhdtx45052ukylkwvu5q05w5lspyjpg37"],["preimage","18264e7cce0b91bfd2016362e8a239591674c0f51ffa152acf5d73edac675432"],["description","{\"id\":\"092cd6341b42604b8e908f5bed45cbd60d98bff33258ab4f83f24a7fad445065\",\"pubkey\":\"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2\",\"created_at\":1724610762,\"kind\":9734,\"tags\":[[\"p\",\"47259076c85f9240e852420d7213c95e95102f1de929fb60f33a2c32570c98c4\"],[\"e\",\"02e52f80e2e6a3ad0993e9c4a7b4e6afc79d067c6ff9c6df3fb2896342dee2df\"],[\"amount\",\"5225000\"],[\"relays\",\"wss://relay.exit.pub\",\"wss://relay.damus.io\",\"wss://nos.lol\",\"wss://relay.mostr.pub\",\"wss://relay.primal.net\"]],\"content\":\"🫂\",\"sig\":\"84a36873000d5003c85c56996be856c598e91f66bf2cae9ee9d984892a11774310acf81eae2b40e9fbf25040b91239e840f856c44b68be2d23e4451fa6c5762a\"}"]],"content":"🫂","sig":"087adfe3c5831e2d760678b2929f35340c35662929acb8050f0956a2a95ba2917bf610f921e3d3fc0c08a123c6f721574eb80ca469fe7e33b6581e976844bfcc"} From 07c364b829f3e31eea791456e838b78269aff68c Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:17:42 -0300 Subject: [PATCH 08/13] test(hydrate): add zap receipt, kind 9735 --- src/storages/hydrate.test.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/storages/hydrate.test.ts b/src/storages/hydrate.test.ts index 3eb70bf8..2d1f74b9 100644 --- a/src/storages/hydrate.test.ts +++ b/src/storages/hydrate.test.ts @@ -143,3 +143,36 @@ Deno.test('hydrateEvents(): report pubkey and post // kind 1984 --- WITHOUT stat }; assertEquals(reportEvent, expectedEvent); }); + +Deno.test('hydrateEvents(): zap sender, zap amount, zapped post // kind 9735 --- WITHOUT stats', async () => { + const relay = new MockRelay(); + await using db = await createTestDB(); + + const zapSender = await eventFixture('kind-0-jack'); + const zapReceipt = await eventFixture('kind-9735-jack-zap-patrick'); + const zappedPost = await eventFixture('kind-1-being-zapped'); + const zapReceiver = await eventFixture('kind-0-patrick'); + + // Save events to database + await relay.event(zapSender); + await relay.event(zapReceipt); + await relay.event(zappedPost); + await relay.event(zapReceiver); + + await hydrateEvents({ + events: [zapReceipt], + store: relay, + kysely: db.kysely, + }); + + const expectedEvent: DittoEvent = { + ...zapReceipt, + zap_sender: zapSender, + zapped: { + ...zappedPost, + author: zapReceiver, + }, + zap_amount: 5225000, // millisats + }; + assertEquals(zapReceipt, expectedEvent); +}); From 7fea3334834c189ba97b244ff8a046f4b32eee19 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:30:01 -0300 Subject: [PATCH 09/13] feat(DittoEvent): add zap_message field --- src/interfaces/DittoEvent.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index 53c0d553..dcaec6ae 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -42,4 +42,5 @@ export interface DittoEvent extends NostrEvent { /** Kind 0 or pubkey that zapped */ zap_sender?: DittoEvent | string; zap_amount?: number; + zap_message?: string; } From 04a9a83fedece9bc51c26001e648f5f5679df21a Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:30:43 -0300 Subject: [PATCH 10/13] feat: hydrate zap_message --- src/storages/hydrate.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 17267391..e2f69e18 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -160,6 +160,8 @@ export function assembleEvents( if (zapSender) { event.zap_sender = b.find((e) => matchFilter({ kinds: [0], authors: [zapSender] }, e)) ?? zapSender; } + + event.zap_message = zapRequest?.content ?? ''; } event.author_stats = stats.authors.find((stats) => stats.pubkey === event.pubkey); From af13614f1aff8aae24fd8733377c0419ff899e9e Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 13:31:49 -0300 Subject: [PATCH 11/13] test(hydrate): expect zap_message also --- src/storages/hydrate.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/storages/hydrate.test.ts b/src/storages/hydrate.test.ts index 2d1f74b9..1527f321 100644 --- a/src/storages/hydrate.test.ts +++ b/src/storages/hydrate.test.ts @@ -173,6 +173,7 @@ Deno.test('hydrateEvents(): zap sender, zap amount, zapped post // kind 9735 --- author: zapReceiver, }, zap_amount: 5225000, // millisats + zap_message: '🫂', }; assertEquals(zapReceipt, expectedEvent); }); From 9b66499df3508d39c52a2404cb8454ed33f807a2 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 14:00:12 -0300 Subject: [PATCH 12/13] fix: get zap recipient in gatherAuthors() function this is needed to work correctly in notifications --- src/storages/hydrate.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index e2f69e18..2de49cb3 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -230,7 +230,13 @@ function gatherQuotes({ events, store, signal }: HydrateOpts): Promise { - const pubkeys = new Set(events.map((event) => event.pubkey)); + const pubkeys = new Set(events.map((event) => { + if (event.kind === 9735) { + const pubkey = event.tags.find(([name]) => name === 'p')?.[1]; + if (pubkey) return pubkey; + } + return event.pubkey; + })); return store.query( [{ kinds: [0], authors: [...pubkeys], limit: pubkeys.size }], From a18b049eb7e8720acc94481b12ddd89b39d8bd16 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 16 Sep 2024 14:08:45 -0300 Subject: [PATCH 13/13] feat: make notifications great again it works the same as before, but with way less code --- src/controllers/api/notifications.ts | 44 ++-------------------------- src/controllers/api/streaming.ts | 27 ++--------------- src/views/mastodon/notifications.ts | 26 +++++++--------- 3 files changed, 15 insertions(+), 82 deletions(-) diff --git a/src/controllers/api/notifications.ts b/src/controllers/api/notifications.ts index 1b9c746a..144064a6 100644 --- a/src/controllers/api/notifications.ts +++ b/src/controllers/api/notifications.ts @@ -4,10 +4,9 @@ import { z } from 'zod'; import { AppContext, AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { DittoPagination } from '@/interfaces/DittoPagination.ts'; -import { getAmount } from '@/utils/bolt11.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { paginated } from '@/utils/api.ts'; -import { renderNotification, RenderNotificationOpts } from '@/views/mastodon/notifications.ts'; +import { renderNotification } from '@/views/mastodon/notifications.ts'; /** Set of known notification types across backends. */ const notificationTypes = new Set([ @@ -86,54 +85,17 @@ async function renderNotifications( const { signal } = c.req.raw; const opts = { signal, limit: params.limit, timeout: Conf.db.timeouts.timelines }; - const zapsRelatedFilter: NostrFilter[] = []; - const events = await store .query(filters, opts) - .then((events) => - events.filter((event) => { - if (event.kind === 9735) { - const zappedEventId = event.tags.find(([name]) => name === 'e')?.[1]; - if (zappedEventId) zapsRelatedFilter.push({ kinds: [1], ids: [zappedEventId] }); - const zapSender = event.tags.find(([name]) => name === 'P')?.[1]; - if (zapSender) zapsRelatedFilter.push({ kinds: [0], authors: [zapSender] }); - } - - return event.pubkey !== pubkey; - }) - ) + .then((events) => events.filter((event) => event.pubkey !== pubkey)) .then((events) => hydrateEvents({ events, store, signal })); if (!events.length) { return c.json([]); } - const zapSendersAndPosts = await store - .query(zapsRelatedFilter, opts) - .then((events) => hydrateEvents({ events, store, signal })); - const notifications = (await Promise.all(events.map((event) => { - const opts: RenderNotificationOpts = { viewerPubkey: pubkey }; - 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; - const zappedPost = event.tags.find(([name]) => name === 'e')?.[1]; - - const amountSchema = z.coerce.number().int().nonnegative().catch(0); - // amount in millisats - const amount = amountSchema.parse(getAmount(event?.tags.find(([name]) => name === 'bolt11')?.[1])); - - opts['zap'] = { - zapSender: zapSendersAndPosts.find(({ pubkey, kind }) => kind === 0 && pubkey === zapSender) ?? zapSender, - zappedPost: zapSendersAndPosts.find(({ id }) => id === zappedPost), - amount, - message: zapRequest?.content, - }; - } - return renderNotification(event, opts); + return renderNotification(event, { viewerPubkey: pubkey }); }))) .filter((notification) => notification && types.has(notification.type)); diff --git a/src/controllers/api/streaming.ts b/src/controllers/api/streaming.ts index 3f05669d..23a94407 100644 --- a/src/controllers/api/streaming.ts +++ b/src/controllers/api/streaming.ts @@ -1,5 +1,5 @@ import TTLCache from '@isaacs/ttlcache'; -import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; +import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import Debug from '@soapbox/stickynotes/debug'; import { z } from 'zod'; @@ -11,10 +11,8 @@ import { getFeedPubkeys } from '@/queries.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { Storages } from '@/storages.ts'; import { bech32ToPubkey, Time } from '@/utils.ts'; -import { getAmount } from '@/utils/bolt11.ts'; import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; import { renderNotification } from '@/views/mastodon/notifications.ts'; -import { RenderNotificationOpts } from '@/views/mastodon/notifications.ts'; const debug = Debug('ditto:streaming'); @@ -157,28 +155,7 @@ const streamingController: AppController = async (c) => { if (['user', 'user:notification'].includes(stream) && pubkey) { sub([{ '#p': [pubkey] }], async (event) => { if (event.pubkey === pubkey) return; // skip own events - - const opts: RenderNotificationOpts = { viewerPubkey: pubkey }; - - 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; - - const amountSchema = z.coerce.number().int().nonnegative().catch(0); - // amount in millisats - const amount = amountSchema.parse(getAmount(event?.tags.find(([name]) => name === 'bolt11')?.[1])); - - opts['zap'] = { - zapSender, - amount, - message: zapRequest?.content, - }; - } - - const payload = await renderNotification(event, opts); + const payload = await renderNotification(event, { viewerPubkey: pubkey }); if (payload) { return { event: 'notification', diff --git a/src/views/mastodon/notifications.ts b/src/views/mastodon/notifications.ts index f2438ad2..973a7a6d 100644 --- a/src/views/mastodon/notifications.ts +++ b/src/views/mastodon/notifications.ts @@ -6,14 +6,8 @@ import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { nostrDate } from '@/utils.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; -export interface RenderNotificationOpts { +interface RenderNotificationOpts { viewerPubkey: string; - zap?: { - zapSender?: NostrEvent | NostrEvent['pubkey']; // kind 0 or pubkey - zappedPost?: NostrEvent; - amount?: number; - message?: string; - }; } function renderNotification(event: DittoEvent, opts: RenderNotificationOpts) { @@ -120,23 +114,23 @@ async function renderNameGrant(event: DittoEvent) { } async function renderZap(event: DittoEvent, opts: RenderNotificationOpts) { - if (!opts.zap?.zapSender) return; + if (!event.zap_sender) return; - const { amount = 0, message = '' } = opts.zap; - if (amount < 1) return; + const { zap_amount = 0, zap_message = '' } = event; + if (zap_amount < 1) return; - const account = typeof opts.zap.zapSender !== 'string' - ? await renderAccount(opts.zap.zapSender) - : await accountFromPubkey(opts.zap.zapSender); + const account = typeof event.zap_sender !== 'string' + ? await renderAccount(event.zap_sender) + : await accountFromPubkey(event.zap_sender); return { id: notificationId(event), type: 'ditto:zap', - amount, - message, + amount: zap_amount, + message: zap_message, created_at: nostrDate(event.created_at).toISOString(), account, - ...(opts.zap?.zappedPost ? { status: await renderStatus(opts.zap?.zappedPost, opts) } : {}), + ...(event.zapped ? { status: await renderStatus(event.zapped, opts) } : {}), }; }