mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
feat: add zaps_amount_cashu to event_stats (with tests)
add zapped_cashu and zaps_amount_cashu field to MastodonStatus
This commit is contained in:
parent
83c96c88b7
commit
7dc56f594b
9 changed files with 95 additions and 0 deletions
|
|
@ -36,6 +36,7 @@ interface EventStatsRow {
|
||||||
quotes_count: number;
|
quotes_count: number;
|
||||||
reactions: string;
|
reactions: string;
|
||||||
zaps_amount: number;
|
zaps_amount: number;
|
||||||
|
zaps_amount_cashu: number;
|
||||||
link_preview?: MastodonPreviewCard;
|
link_preview?: MastodonPreviewCard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('event_stats')
|
||||||
|
.addColumn('zaps_amount_cashu', 'integer', (col) => col.notNull().defaultTo(0))
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
||||||
|
await db.schema.alterTable('event_stats').dropColumn('zaps_amount_cashu').execute();
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ export interface EventStats {
|
||||||
quotes_count: number;
|
quotes_count: number;
|
||||||
reactions: Record<string, number>;
|
reactions: Record<string, number>;
|
||||||
zaps_amount: number;
|
zaps_amount: number;
|
||||||
|
zaps_amount_cashu: number;
|
||||||
link_preview?: MastodonPreviewCard;
|
link_preview?: MastodonPreviewCard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -448,6 +448,7 @@ export class DittoRelayStore implements NRelay {
|
||||||
quotes_count: 0,
|
quotes_count: 0,
|
||||||
reactions: '{}',
|
reactions: '{}',
|
||||||
zaps_amount: 0,
|
zaps_amount: 0,
|
||||||
|
zaps_amount_cashu: 0,
|
||||||
link_preview: linkPreview,
|
link_preview: linkPreview,
|
||||||
})
|
})
|
||||||
.onConflict((oc) => oc.column('event_id').doUpdateSet({ link_preview: linkPreview }))
|
.onConflict((oc) => oc.column('event_id').doUpdateSet({ link_preview: linkPreview }))
|
||||||
|
|
|
||||||
|
|
@ -411,6 +411,7 @@ async function gatherEventStats(
|
||||||
quotes_count: Math.max(0, row.quotes_count),
|
quotes_count: Math.max(0, row.quotes_count),
|
||||||
reactions: row.reactions,
|
reactions: row.reactions,
|
||||||
zaps_amount: Math.max(0, row.zaps_amount),
|
zaps_amount: Math.max(0, row.zaps_amount),
|
||||||
|
zaps_amount_cashu: Math.max(0, row.zaps_amount_cashu),
|
||||||
link_preview: row.link_preview,
|
link_preview: row.link_preview,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,48 @@ Deno.test('updateStats with kind 7 increments reactions count', async () => {
|
||||||
assertEquals(stats!.reactions_count, 3);
|
assertEquals(stats!.reactions_count, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test('updateStats with kind 9321 increments zaps_amount_cashu count', async () => {
|
||||||
|
await using test = await setupTest();
|
||||||
|
const { kysely, relay } = test;
|
||||||
|
|
||||||
|
const note = genEvent({ kind: 1 });
|
||||||
|
await relay.event(note);
|
||||||
|
|
||||||
|
await updateStats({
|
||||||
|
...test,
|
||||||
|
event: genEvent({
|
||||||
|
kind: 9321,
|
||||||
|
content: 'Do you love me?',
|
||||||
|
tags: [
|
||||||
|
['e', note.id],
|
||||||
|
[
|
||||||
|
'proof',
|
||||||
|
'{"id":"004f7adf2a04356c","amount":29,"secret":"6780378b186cf7ada639ce4807803ad5e4a71217688430512f35074f9bca99c0","C":"03f0dd8df04427c8c53e4ae9ce8eb91c4880203d6236d1d745c788a5d7a47aaff3","dleq":{"e":"bd22fcdb7ede1edb52b9b8c6e1194939112928e7b4fc0176325e7671fb2bd351","s":"a9ad015571a0e538d62966a16d2facf806fb956c746a3dfa41fa689486431c67","r":"b283980e30bf5a31a45e5e296e93ae9f20bf3a140c884b3b4cd952dbecc521df"}}',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateStats({
|
||||||
|
...test,
|
||||||
|
event: genEvent({
|
||||||
|
kind: 9321,
|
||||||
|
content: 'Ultimatum',
|
||||||
|
tags: [
|
||||||
|
['e', note.id],
|
||||||
|
[
|
||||||
|
'proof',
|
||||||
|
'{"id":"004f7adf2a04356c","amount":100,"secret":"6780378b186cf7ada639ce4807803ad5e4a71217688430512f35074f9bca99c0","C":"03f0dd8df04427c8c53e4ae9ce8eb91c4880203d6236d1d745c788a5d7a47aaff3","dleq":{"e":"bd22fcdb7ede1edb52b9b8c6e1194939112928e7b4fc0176325e7671fb2bd351","s":"a9ad015571a0e538d62966a16d2facf806fb956c746a3dfa41fa689486431c67","r":"b283980e30bf5a31a45e5e296e93ae9f20bf3a140c884b3b4cd952dbecc521df"}}',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const stats = await getEventStats(kysely, note.id);
|
||||||
|
|
||||||
|
assertEquals(stats!.zaps_amount_cashu, 129);
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test('updateStats with kind 5 decrements reactions count', async () => {
|
Deno.test('updateStats with kind 5 decrements reactions count', async () => {
|
||||||
await using test = await setupTest();
|
await using test = await setupTest();
|
||||||
const { kysely, relay } = test;
|
const { kysely, relay } = test;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { type Proof } from '@cashu/cashu-ts';
|
||||||
|
import { proofSchema } from '@ditto/cashu';
|
||||||
import { DittoTables } from '@ditto/db';
|
import { DittoTables } from '@ditto/db';
|
||||||
import { NostrEvent, NSchema as n, NStore } from '@nostrify/nostrify';
|
import { NostrEvent, NSchema as n, NStore } from '@nostrify/nostrify';
|
||||||
import { Insertable, Kysely, UpdateObject } from 'kysely';
|
import { Insertable, Kysely, UpdateObject } from 'kysely';
|
||||||
|
|
@ -38,6 +40,8 @@ export async function updateStats(opts: UpdateStatsOpts): Promise<void> {
|
||||||
return handleEvent7(opts);
|
return handleEvent7(opts);
|
||||||
case 9735:
|
case 9735:
|
||||||
return handleEvent9735(opts);
|
return handleEvent9735(opts);
|
||||||
|
case 9321:
|
||||||
|
return handleEvent9321(opts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,6 +236,32 @@ async function handleEvent9735(opts: UpdateStatsOpts): Promise<void> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Update stats for kind 9321 event. */
|
||||||
|
async function handleEvent9321(opts: UpdateStatsOpts): Promise<void> {
|
||||||
|
const { kysely, event } = opts;
|
||||||
|
|
||||||
|
// https://github.com/nostr-protocol/nips/blob/master/61.md#nutzap-event
|
||||||
|
// It's possible to nutzap a profile without nutzapping a post, but we don't care about this case
|
||||||
|
const id = event.tags.find(([name]) => name === 'e')?.[1];
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
const proofs = (event.tags.filter(([name]) => name === 'proof').map(([_, proof]) => {
|
||||||
|
const { success, data } = n.json().pipe(proofSchema).safeParse(proof);
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.filter(Boolean)) as Proof[];
|
||||||
|
|
||||||
|
const amount = proofs.reduce((prev, current) => prev + current.amount, 0);
|
||||||
|
|
||||||
|
await updateEventStats(
|
||||||
|
kysely,
|
||||||
|
id,
|
||||||
|
({ zaps_amount_cashu }) => ({ zaps_amount_cashu: Math.max(0, zaps_amount_cashu + amount) }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** Get the pubkeys that were added and removed from a follow event. */
|
/** Get the pubkeys that were added and removed from a follow event. */
|
||||||
export function getFollowDiff(
|
export function getFollowDiff(
|
||||||
tags: string[][],
|
tags: string[][],
|
||||||
|
|
@ -318,6 +348,7 @@ export async function updateEventStats(
|
||||||
reactions_count: 0,
|
reactions_count: 0,
|
||||||
quotes_count: 0,
|
quotes_count: 0,
|
||||||
zaps_amount: 0,
|
zaps_amount: 0,
|
||||||
|
zaps_amount_cashu: 0,
|
||||||
reactions: '{}',
|
reactions: '{}',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ async function renderStatus(
|
||||||
? await store.query([
|
? await store.query([
|
||||||
{ kinds: [6], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
{ kinds: [6], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
{ kinds: [7], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
{ kinds: [7], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
|
{ kinds: [9321], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
{ kinds: [9734], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
{ kinds: [9734], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
{ kinds: [10001], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
{ kinds: [10001], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
{ kinds: [10003], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
{ kinds: [10003], '#e': [event.id], authors: [viewerPubkey], limit: 1 },
|
||||||
|
|
@ -80,6 +81,7 @@ async function renderStatus(
|
||||||
const pinEvent = relatedEvents.find((event) => event.kind === 10001);
|
const pinEvent = relatedEvents.find((event) => event.kind === 10001);
|
||||||
const bookmarkEvent = relatedEvents.find((event) => event.kind === 10003);
|
const bookmarkEvent = relatedEvents.find((event) => event.kind === 10003);
|
||||||
const zapEvent = relatedEvents.find((event) => event.kind === 9734);
|
const zapEvent = relatedEvents.find((event) => event.kind === 9734);
|
||||||
|
const nutzapEvent = relatedEvents.find((event) => event.kind === 9321);
|
||||||
|
|
||||||
const compatMentions = buildInlineRecipients(mentions.filter((m) => {
|
const compatMentions = buildInlineRecipients(mentions.filter((m) => {
|
||||||
if (m.id === account.id) return false;
|
if (m.id === account.id) return false;
|
||||||
|
|
@ -136,6 +138,7 @@ async function renderStatus(
|
||||||
reblogs_count: event.event_stats?.reposts_count ?? 0,
|
reblogs_count: event.event_stats?.reposts_count ?? 0,
|
||||||
favourites_count: event.event_stats?.reactions['+'] ?? 0,
|
favourites_count: event.event_stats?.reactions['+'] ?? 0,
|
||||||
zaps_amount: event.event_stats?.zaps_amount ?? 0,
|
zaps_amount: event.event_stats?.zaps_amount ?? 0,
|
||||||
|
zaps_amount_cashu: event.event_stats?.zaps_amount_cashu ?? 0,
|
||||||
favourited: reactionEvent?.content === '+',
|
favourited: reactionEvent?.content === '+',
|
||||||
reblogged: Boolean(repostEvent),
|
reblogged: Boolean(repostEvent),
|
||||||
muted: false,
|
muted: false,
|
||||||
|
|
@ -155,6 +158,7 @@ async function renderStatus(
|
||||||
uri: Conf.local(`/users/${account.acct}/statuses/${event.id}`),
|
uri: Conf.local(`/users/${account.acct}/statuses/${event.id}`),
|
||||||
url: Conf.local(`/@${account.acct}/${event.id}`),
|
url: Conf.local(`/@${account.acct}/${event.id}`),
|
||||||
zapped: Boolean(zapEvent),
|
zapped: Boolean(zapEvent),
|
||||||
|
zapped_cashu: Boolean(nutzapEvent),
|
||||||
ditto: {
|
ditto: {
|
||||||
external_url: Conf.external(nevent),
|
external_url: Conf.external(nevent),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export interface MastodonStatus {
|
||||||
reblogs_count: number;
|
reblogs_count: number;
|
||||||
favourites_count: number;
|
favourites_count: number;
|
||||||
zaps_amount: number;
|
zaps_amount: number;
|
||||||
|
zaps_amount_cashu: number;
|
||||||
favourited: boolean;
|
favourited: boolean;
|
||||||
reblogged: boolean;
|
reblogged: boolean;
|
||||||
muted: boolean;
|
muted: boolean;
|
||||||
|
|
@ -35,6 +36,7 @@ export interface MastodonStatus {
|
||||||
uri: string;
|
uri: string;
|
||||||
url: string;
|
url: string;
|
||||||
zapped: boolean;
|
zapped: boolean;
|
||||||
|
zapped_cashu: boolean;
|
||||||
pleroma: {
|
pleroma: {
|
||||||
emoji_reactions: { name: string; count: number; me: boolean }[];
|
emoji_reactions: { name: string; count: number; me: boolean }[];
|
||||||
expires_at?: string;
|
expires_at?: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue