Merge branch 'feat-zap-counter' into 'main'
Create zap counter and zapped_by endpoint See merge request soapbox-pub/ditto!371
This commit is contained in:
commit
e912210589
|
@ -88,6 +88,7 @@ import {
|
||||||
unpinController,
|
unpinController,
|
||||||
unreblogStatusController,
|
unreblogStatusController,
|
||||||
zapController,
|
zapController,
|
||||||
|
zappedByController,
|
||||||
} from '@/controllers/api/statuses.ts';
|
} from '@/controllers/api/statuses.ts';
|
||||||
import { streamingController } from '@/controllers/api/streaming.ts';
|
import { streamingController } from '@/controllers/api/streaming.ts';
|
||||||
import { suggestionsV1Controller, suggestionsV2Controller } from '@/controllers/api/suggestions.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.get('/api/v1/ditto/names', requireSigner, nameRequestsController);
|
||||||
|
|
||||||
app.post('/api/v1/ditto/zap', requireSigner, zapController);
|
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.post('/api/v1/reports', requireSigner, reportController);
|
||||||
app.get('/api/v1/admin/reports', requireSigner, requireRole('admin'), adminReportsController);
|
app.get('/api/v1/admin/reports', requireSigner, requireRole('admin'), adminReportsController);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { lookupPubkey } from '@/utils/lookup.ts';
|
||||||
import { addTag, deleteTag } from '@/utils/tags.ts';
|
import { addTag, deleteTag } from '@/utils/tags.ts';
|
||||||
import { asyncReplaceAll } from '@/utils/text.ts';
|
import { asyncReplaceAll } from '@/utils/text.ts';
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
|
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
|
|
||||||
const createStatusSchema = z.object({
|
const createStatusSchema = z.object({
|
||||||
in_reply_to_id: n.id().nullish(),
|
in_reply_to_id: n.id().nullish(),
|
||||||
|
@ -541,6 +542,40 @@ 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];
|
||||||
|
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 = 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 {
|
||||||
|
comment,
|
||||||
|
amount,
|
||||||
|
account,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
)).filter(Boolean);
|
||||||
|
|
||||||
|
return c.json(results);
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
bookmarkController,
|
bookmarkController,
|
||||||
contextController,
|
contextController,
|
||||||
|
@ -557,4 +592,5 @@ export {
|
||||||
unpinController,
|
unpinController,
|
||||||
unreblogStatusController,
|
unreblogStatusController,
|
||||||
zapController,
|
zapController,
|
||||||
|
zappedByController,
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,6 +23,7 @@ interface EventStatsRow {
|
||||||
reactions_count: number;
|
reactions_count: number;
|
||||||
quotes_count: number;
|
quotes_count: number;
|
||||||
reactions: string;
|
reactions: string;
|
||||||
|
zaps_amount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EventRow {
|
interface EventRow {
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Kysely } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema
|
||||||
|
.alterTable('event_stats')
|
||||||
|
.addColumn('zaps_amount', 'integer', (col) => col.notNull().defaultTo(0))
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await db.schema.alterTable('event_stats').dropColumn('zaps_amount').execute();
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ export interface EventStats {
|
||||||
reposts_count: number;
|
reposts_count: number;
|
||||||
quotes_count: number;
|
quotes_count: number;
|
||||||
reactions: Record<string, number>;
|
reactions: Record<string, number>;
|
||||||
|
zaps_amount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Internal Event representation used by Ditto, including extra keys. */
|
/** Internal Event representation used by Ditto, including extra keys. */
|
||||||
|
|
|
@ -330,6 +330,7 @@ async function gatherEventStats(events: DittoEvent[]): Promise<DittoTables['even
|
||||||
reactions_count: Math.max(0, row.reactions_count),
|
reactions_count: Math.max(0, row.reactions_count),
|
||||||
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),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { NostrEvent, NStore } from '@nostrify/nostrify';
|
import { NostrEvent, NSchema as n, NStore } from '@nostrify/nostrify';
|
||||||
import { Kysely, UpdateObject } from 'kysely';
|
import { Kysely, UpdateObject } from 'kysely';
|
||||||
import { SetRequired } from 'type-fest';
|
import { SetRequired } from 'type-fest';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { DittoTables } from '@/db/DittoTables.ts';
|
import { DittoTables } from '@/db/DittoTables.ts';
|
||||||
import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
|
import { findQuoteTag, findReplyTag, getTagSet } from '@/utils/tags.ts';
|
||||||
|
@ -27,6 +28,8 @@ export async function updateStats({ event, kysely, store, x = 1 }: UpdateStatsOp
|
||||||
return handleEvent6(kysely, event, x);
|
return handleEvent6(kysely, event, x);
|
||||||
case 7:
|
case 7:
|
||||||
return handleEvent7(kysely, event, x);
|
return handleEvent7(kysely, event, x);
|
||||||
|
case 9735:
|
||||||
|
return handleEvent9735(kysely, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,6 +135,29 @@ async function handleEvent7(kysely: Kysely<DittoTables>, event: NostrEvent, x: n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Update stats for kind 9735 event. */
|
||||||
|
async function handleEvent9735(kysely: Kysely<DittoTables>, event: NostrEvent): Promise<void> {
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
const amountSchema = z.coerce.number().int().nonnegative().catch(0);
|
||||||
|
let amount = 0;
|
||||||
|
try {
|
||||||
|
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 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateEventStats(
|
||||||
|
kysely,
|
||||||
|
id,
|
||||||
|
({ zaps_amount }) => ({ zaps_amount: Math.max(0, zaps_amount + 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[][],
|
||||||
|
@ -219,6 +245,7 @@ export async function updateEventStats(
|
||||||
reposts_count: 0,
|
reposts_count: 0,
|
||||||
reactions_count: 0,
|
reactions_count: 0,
|
||||||
quotes_count: 0,
|
quotes_count: 0,
|
||||||
|
zaps_amount: 0,
|
||||||
reactions: '{}',
|
reactions: '{}',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,7 @@ async function renderStatus(event: DittoEvent, opts: RenderStatusOpts): Promise<
|
||||||
replies_count: event.event_stats?.replies_count ?? 0,
|
replies_count: event.event_stats?.replies_count ?? 0,
|
||||||
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,
|
||||||
favourited: reactionEvent?.content === '+',
|
favourited: reactionEvent?.content === '+',
|
||||||
reblogged: Boolean(repostEvent),
|
reblogged: Boolean(repostEvent),
|
||||||
muted: false,
|
muted: false,
|
||||||
|
|
Loading…
Reference in New Issue