From 1f4de9aed0428775a8956178de2c48fb8cb139f9 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 8 Jun 2024 08:57:16 -0300 Subject: [PATCH 01/10] feat: add migration for 'zaps_amount' column --- src/db/migrations/025_event_stats_add_zap_count.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/db/migrations/025_event_stats_add_zap_count.ts diff --git a/src/db/migrations/025_event_stats_add_zap_count.ts b/src/db/migrations/025_event_stats_add_zap_count.ts new file mode 100644 index 0000000..9147990 --- /dev/null +++ b/src/db/migrations/025_event_stats_add_zap_count.ts @@ -0,0 +1,12 @@ +import { Kysely } from 'kysely'; + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable('event_stats') + .addColumn('zaps_amount', 'integer', (col) => col.notNull().defaultTo(0)) + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.alterTable('event_stats').dropColumn('zaps_amount').execute(); +} From d2608256600d12a2b5f8c60a3524435b943d11e3 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Sat, 8 Jun 2024 09:08:14 -0300 Subject: [PATCH 02/10] feat: add 'zaps_amount' to EventStatsRow as number --- src/db/DittoTables.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/db/DittoTables.ts b/src/db/DittoTables.ts index d9e320d..aed8c8c 100644 --- a/src/db/DittoTables.ts +++ b/src/db/DittoTables.ts @@ -23,6 +23,7 @@ interface EventStatsRow { reactions_count: number; quotes_count: number; reactions: string; + zaps_amount: number; } interface EventRow { From 4b58fb9bf2ee4807640d3731fd98e40751dbf723 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 10 Jun 2024 09:33:24 -0300 Subject: [PATCH 03/10] feat(updateStats): handle kind 9735 --- src/utils/stats.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/utils/stats.ts b/src/utils/stats.ts index 3cc82cf..8f0b550 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -1,7 +1,7 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; import { Kysely, UpdateObject } from 'kysely'; -import { SetRequired } from 'type-fest'; +import { SetRequired } from 'type-fest'; import { DittoTables } from '@/db/DittoTables.ts'; import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; import { Conf } from '@/config.ts'; @@ -27,6 +27,8 @@ export async function updateStats({ event, kysely, store, x = 1 }: UpdateStatsOp return handleEvent6(kysely, event, x); case 7: return handleEvent7(kysely, event, x); + case 9735: + return handleEvent9735(kysely, event); } } @@ -132,6 +134,28 @@ async function handleEvent7(kysely: Kysely, event: NostrEvent, x: n } } +/** Update stats for kind 9735 event. */ +async function handleEvent9735(kysely: Kysely, event: NostrEvent): Promise { + // https://github.com/nostr-protocol/nips/blob/master/57.md#appendix-f-validating-zap-receipts + const id = event.tags.find(([name]) => name === 'e')?.[1]; + if (!id) return; + + let amount = '0'; + try { + const zapRequest = JSON.parse(event.tags.find(([name]) => name === 'description')?.[1]!) as NostrEvent; + amount = zapRequest.tags.find(([name]) => name === 'amount')?.[1]!; + } catch { + return; + } + if (amount === '0' || !amount || (/^\d+$/).test(amount) === false) return; + + await updateEventStats( + kysely, + id, + ({ zaps_amount }) => ({ zaps_amount: Math.max(0, zaps_amount + Number(amount)) }), + ); +} + /** Get the pubkeys that were added and removed from a follow event. */ export function getFollowDiff( tags: string[][], @@ -219,6 +243,7 @@ export async function updateEventStats( reposts_count: 0, reactions_count: 0, quotes_count: 0, + zaps_amount: 0, reactions: '{}', }; From 18648f7be37154e065ef7d5f34f6577cf241bf1a Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 10 Jun 2024 10:00:58 -0300 Subject: [PATCH 04/10] fix(hydrate): return zaps_amount in gatherEventStats --- src/storages/hydrate.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/storages/hydrate.ts b/src/storages/hydrate.ts index 9ec9e8c..3c26432 100644 --- a/src/storages/hydrate.ts +++ b/src/storages/hydrate.ts @@ -330,6 +330,7 @@ async function gatherEventStats(events: DittoEvent[]): Promise Date: Mon, 10 Jun 2024 10:38:56 -0300 Subject: [PATCH 05/10] fix: add zaps_amount to EventStats --- src/interfaces/DittoEvent.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/interfaces/DittoEvent.ts b/src/interfaces/DittoEvent.ts index 85e11d6..2f2aef2 100644 --- a/src/interfaces/DittoEvent.ts +++ b/src/interfaces/DittoEvent.ts @@ -13,6 +13,7 @@ export interface EventStats { reposts_count: number; quotes_count: number; reactions: Record; + zaps_amount: number; } /** Internal Event representation used by Ditto, including extra keys. */ From 0b49ee4fa691db90dfe1229f499d81d9957ab844 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Mon, 10 Jun 2024 10:39:34 -0300 Subject: [PATCH 06/10] feat(renderStatus): return zaps_amount --- src/views/mastodon/statuses.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/mastodon/statuses.ts b/src/views/mastodon/statuses.ts index ed14c8e..a0874b3 100644 --- a/src/views/mastodon/statuses.ts +++ b/src/views/mastodon/statuses.ts @@ -104,6 +104,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise< replies_count: event.event_stats?.replies_count ?? 0, reblogs_count: event.event_stats?.reposts_count ?? 0, favourites_count: event.event_stats?.reactions['+'] ?? 0, + zaps_amount: event.event_stats?.zaps_amount ?? 0, favourited: reactionEvent?.content === '+', reblogged: Boolean(repostEvent), muted: false, From b43aed2301bc535de378824eb9a903c5c0124d45 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 13 Jun 2024 00:48:31 -0300 Subject: [PATCH 07/10] feat: create zappedByController --- src/controllers/api/statuses.ts | 39 +++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index ce4cbfa..4945e3b 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -20,6 +20,7 @@ import { lookupPubkey } from '@/utils/lookup.ts'; import { addTag, deleteTag } from '@/utils/tags.ts'; import { asyncReplaceAll } from '@/utils/text.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts'; +import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; const createStatusSchema = z.object({ in_reply_to_id: n.id().nullish(), @@ -541,6 +542,43 @@ const zapController: AppController = async (c) => { } }; +const zappedByController: AppController = async (c) => { + const id = c.req.param('id'); + const store = await Storages.db(); + + const events: DittoEvent[] = (await store.query([{ kinds: [9735], '#e': [id], limit: 100 }])).map((event) => { + const zapRequest = event.tags.find(([name]) => name === 'description')?.[1]; + if (!zapRequest) return; + try { + return JSON.parse(zapRequest); + } catch { + return; + } + }).filter(Boolean); + + await hydrateEvents({ events, store }); + + const results = (await Promise.all( + events.map(async (event) => { + const amount = event.tags.find(([name]) => name === 'amount')?.[1]; + const onlyDigits = /^\d+$/; + if (!amount || !onlyDigits.test(amount)) return; + + const comment = event?.content ?? ''; + + const account = event?.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey); + + return { + zap_comment: comment, + zap_amount: Number(amount), + account, + }; + }), + )).filter(Boolean); + + return c.json(results); +}; + export { bookmarkController, contextController, @@ -557,4 +595,5 @@ export { unpinController, unreblogStatusController, zapController, + zappedByController, }; From 7474c1b28836dc8715abc9227594b91df9afd1e7 Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 13 Jun 2024 00:49:21 -0300 Subject: [PATCH 08/10] feat: add /api/v1/ditto/statuses/:id{[0-9a-f]{64}}/zapped_by endpoint --- src/app.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app.ts b/src/app.ts index 5ab31d8..276f9ee 100644 --- a/src/app.ts +++ b/src/app.ts @@ -88,6 +88,7 @@ import { unpinController, unreblogStatusController, zapController, + zappedByController, } from '@/controllers/api/statuses.ts'; import { streamingController } from '@/controllers/api/streaming.ts'; import { suggestionsV1Controller, suggestionsV2Controller } from '@/controllers/api/suggestions.ts'; @@ -259,6 +260,7 @@ app.post('/api/v1/ditto/names', requireSigner, nameRequestController); app.get('/api/v1/ditto/names', requireSigner, nameRequestsController); app.post('/api/v1/ditto/zap', requireSigner, zapController); +app.get('/api/v1/ditto/statuses/:id{[0-9a-f]{64}}/zapped_by', zappedByController); app.post('/api/v1/reports', requireSigner, reportController); app.get('/api/v1/admin/reports', requireSigner, requireRole('admin'), adminReportsController); From 22dbddb5d3bab6e0ea79beaa7eca4d1c4cf0c1df Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 13 Jun 2024 14:21:01 -0300 Subject: [PATCH 09/10] refactor: zap amount parsed with zod, change zapped_by fields name --- src/controllers/api/statuses.ts | 11 ++++------- src/utils/stats.ts | 12 +++++++----- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index 4945e3b..7b9e82c 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -545,6 +545,7 @@ const zapController: AppController = async (c) => { const zappedByController: AppController = async (c) => { const id = c.req.param('id'); const store = await Storages.db(); + const amountSchema = z.coerce.number().int().nonnegative().catch(0); const events: DittoEvent[] = (await store.query([{ kinds: [9735], '#e': [id], limit: 100 }])).map((event) => { const zapRequest = event.tags.find(([name]) => name === 'description')?.[1]; @@ -560,17 +561,13 @@ const zappedByController: AppController = async (c) => { const results = (await Promise.all( events.map(async (event) => { - const amount = event.tags.find(([name]) => name === 'amount')?.[1]; - const onlyDigits = /^\d+$/; - if (!amount || !onlyDigits.test(amount)) return; - + const amount = amountSchema.parse(event.tags.find(([name]) => name === 'amount')?.[1]); const comment = event?.content ?? ''; - const account = event?.author ? await renderAccount(event.author) : await accountFromPubkey(event.pubkey); return { - zap_comment: comment, - zap_amount: Number(amount), + comment, + amount, account, }; }), diff --git a/src/utils/stats.ts b/src/utils/stats.ts index 8f0b550..e6001d1 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -1,7 +1,8 @@ import { NostrEvent, NStore } from '@nostrify/nostrify'; import { Kysely, UpdateObject } from 'kysely'; - import { SetRequired } from 'type-fest'; +import { z } from 'zod'; + import { DittoTables } from '@/db/DittoTables.ts'; import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts'; import { Conf } from '@/config.ts'; @@ -140,19 +141,20 @@ async function handleEvent9735(kysely: Kysely, event: NostrEvent): const id = event.tags.find(([name]) => name === 'e')?.[1]; if (!id) return; - let amount = '0'; + const amountSchema = z.coerce.number().int().nonnegative().catch(0); + let amount = 0; try { const zapRequest = JSON.parse(event.tags.find(([name]) => name === 'description')?.[1]!) as NostrEvent; - amount = zapRequest.tags.find(([name]) => name === 'amount')?.[1]!; + amount = amountSchema.parse(zapRequest.tags.find(([name]) => name === 'amount')?.[1]); + if (amount <= 0) return; } catch { return; } - if (amount === '0' || !amount || (/^\d+$/).test(amount) === false) return; await updateEventStats( kysely, id, - ({ zaps_amount }) => ({ zaps_amount: Math.max(0, zaps_amount + Number(amount)) }), + ({ zaps_amount }) => ({ zaps_amount: Math.max(0, zaps_amount + amount) }), ); } From 880b09e016d3c507a91883454b9c3aaf77239b9c Mon Sep 17 00:00:00 2001 From: "P. Reis" Date: Thu, 13 Jun 2024 15:51:13 -0300 Subject: [PATCH 10/10] refactor: parse zap request with NSchema --- src/utils/stats.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/stats.ts b/src/utils/stats.ts index e6001d1..ccba0a5 100644 --- a/src/utils/stats.ts +++ b/src/utils/stats.ts @@ -1,4 +1,4 @@ -import { NostrEvent, NStore } from '@nostrify/nostrify'; +import { NostrEvent, NSchema as n, NStore } from '@nostrify/nostrify'; import { Kysely, UpdateObject } from 'kysely'; import { SetRequired } from 'type-fest'; import { z } from 'zod'; @@ -144,7 +144,7 @@ async function handleEvent9735(kysely: Kysely, event: NostrEvent): const amountSchema = z.coerce.number().int().nonnegative().catch(0); let amount = 0; try { - const zapRequest = JSON.parse(event.tags.find(([name]) => name === 'description')?.[1]!) as NostrEvent; + const zapRequest = n.json().pipe(n.event()).parse(event.tags.find(([name]) => name === 'description')?.[1]); amount = amountSchema.parse(zapRequest.tags.find(([name]) => name === 'amount')?.[1]); if (amount <= 0) return; } catch {