ditto/src/storages/hydrate.ts

222 lines
5.7 KiB
TypeScript
Raw Normal View History

import { NostrEvent, NStore } from '@nostrify/nostrify';
2024-04-30 18:43:53 -05:00
import { matchFilter } from 'nostr-tools';
2024-04-23 19:31:40 -05:00
2024-03-30 17:44:17 -05:00
import { db } from '@/db.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
2024-04-23 19:31:40 -05:00
import { DittoTables } from '@/db/DittoTables.ts';
import { Conf } from '@/config.ts';
2024-04-23 19:31:40 -05:00
interface HydrateOpts {
events: DittoEvent[];
2024-01-23 14:06:16 -06:00
storage: NStore;
2024-01-04 02:09:23 -06:00
signal?: AbortSignal;
}
/** Hydrate events using the provided storage. */
2024-04-23 19:31:40 -05:00
async function hydrateEvents(opts: HydrateOpts): Promise<DittoEvent[]> {
const { events, storage, signal } = opts;
if (!events.length) {
return events;
}
2024-04-23 19:31:40 -05:00
const cache = [...events];
2024-04-23 20:11:07 -05:00
for (const event of await gatherReposts({ events: cache, storage, signal })) {
2024-04-23 19:31:40 -05:00
cache.push(event);
}
for (const event of await gatherReacted({ events: cache, storage, signal })) {
cache.push(event);
}
2024-04-23 20:11:07 -05:00
for (const event of await gatherQuotes({ events: cache, storage, signal })) {
2024-04-23 19:31:40 -05:00
cache.push(event);
}
2024-04-23 20:11:07 -05:00
for (const event of await gatherAuthors({ events: cache, storage, signal })) {
2024-04-23 19:31:40 -05:00
cache.push(event);
}
2024-04-23 20:11:07 -05:00
for (const event of await gatherUsers({ events: cache, storage, signal })) {
2024-04-23 19:31:40 -05:00
cache.push(event);
}
const stats = {
authors: await gatherAuthorStats(cache),
events: await gatherEventStats(cache),
2024-04-23 19:31:40 -05:00
};
2024-04-23 20:11:07 -05:00
// Dedupe events.
const results = [...new Map(cache.map((event) => [event.id, event])).values()];
// First connect all the events to each-other, then connect the connected events to the original list.
assembleEvents(results, results, stats);
assembleEvents(events, results, stats);
return events;
}
2024-04-23 20:11:07 -05:00
/** Connect the events in list `b` to the DittoEvent fields in list `a`. */
2024-04-23 19:31:40 -05:00
function assembleEvents(
a: DittoEvent[],
b: DittoEvent[],
stats: { authors: DittoTables['author_stats'][]; events: DittoTables['event_stats'][] },
): DittoEvent[] {
const admin = Conf.pubkey;
for (const event of a) {
event.author = b.find((e) => matchFilter({ kinds: [0], authors: [event.pubkey] }, e));
event.user = b.find((e) => matchFilter({ kinds: [30361], authors: [admin], '#d': [event.pubkey] }, e));
2024-04-23 19:31:40 -05:00
if (event.kind === 6) {
const id = event.tags.find(([name]) => name === 'e')?.[1];
if (id) {
event.repost = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e));
}
2024-04-23 19:31:40 -05:00
}
2024-04-23 19:31:40 -05:00
if (event.kind === 1) {
const id = event.tags.find(([name]) => name === 'q')?.[1];
if (id) {
event.quote_repost = b.find((e) => matchFilter({ kinds: [1], ids: [id] }, e));
}
2024-04-23 19:31:40 -05:00
}
2024-04-23 19:31:40 -05:00
event.author_stats = stats.authors.find((stats) => stats.pubkey === event.pubkey);
event.event_stats = stats.events.find((stats) => stats.event_id === event.id);
}
2024-04-23 19:31:40 -05:00
return a;
}
2024-04-23 20:11:07 -05:00
/** Collect reposts from the events. */
2024-04-23 19:31:40 -05:00
function gatherReposts({ events, storage, signal }: HydrateOpts): Promise<DittoEvent[]> {
const ids = new Set<string>();
2024-03-30 17:44:17 -05:00
for (const event of events) {
2024-04-23 19:31:40 -05:00
if (event.kind === 6) {
const id = event.tags.find(([name]) => name === 'e')?.[1];
if (id) {
ids.add(id);
}
2024-03-30 17:44:17 -05:00
}
}
2024-04-23 19:31:40 -05:00
return storage.query(
[{ ids: [...ids], limit: ids.size }],
{ signal },
);
2024-03-30 17:44:17 -05:00
}
/** Collect events being reacted to by the events. */
function gatherReacted({ events, storage, signal }: HydrateOpts): Promise<DittoEvent[]> {
const ids = new Set<string>();
for (const event of events) {
if (event.kind === 7) {
const id = event.tags.find(([name]) => name === 'e')?.[1];
if (id) {
ids.add(id);
}
}
}
return storage.query(
[{ ids: [...ids], limit: ids.size }],
{ signal },
);
}
2024-04-23 20:11:07 -05:00
/** Collect quotes from the events. */
2024-04-23 19:31:40 -05:00
function gatherQuotes({ events, storage, signal }: HydrateOpts): Promise<DittoEvent[]> {
const ids = new Set<string>();
2024-03-30 17:44:17 -05:00
for (const event of events) {
2024-04-23 19:31:40 -05:00
if (event.kind === 1) {
const id = event.tags.find(([name]) => name === 'q')?.[1];
if (id) {
ids.add(id);
}
2024-03-30 17:44:17 -05:00
}
}
2024-04-23 19:31:40 -05:00
return storage.query(
[{ ids: [...ids], limit: ids.size }],
{ signal },
);
}
2024-04-23 20:11:07 -05:00
/** Collect authors from the events. */
2024-04-23 19:31:40 -05:00
function gatherAuthors({ events, storage, signal }: HydrateOpts): Promise<DittoEvent[]> {
const pubkeys = new Set(events.map((event) => event.pubkey));
return storage.query(
[{ kinds: [0], authors: [...pubkeys], limit: pubkeys.size }],
{ signal },
);
}
2024-04-23 20:11:07 -05:00
/** Collect users from the events. */
2024-04-23 19:31:40 -05:00
function gatherUsers({ events, storage, signal }: HydrateOpts): Promise<DittoEvent[]> {
const pubkeys = new Set(events.map((event) => event.pubkey));
return storage.query(
[{ kinds: [30361], authors: [Conf.pubkey], '#d': [...pubkeys], limit: pubkeys.size }],
{ signal },
);
}
2024-04-23 20:11:07 -05:00
/** Collect author stats from the events. */
2024-04-23 19:31:40 -05:00
function gatherAuthorStats(events: DittoEvent[]): Promise<DittoTables['author_stats'][]> {
const pubkeys = new Set<string>(
events
.filter((event) => event.kind === 0)
.map((event) => event.pubkey),
);
if (!pubkeys.size) {
return Promise.resolve([]);
}
2024-04-23 19:31:40 -05:00
return db
.selectFrom('author_stats')
.selectAll()
.where('pubkey', 'in', [...pubkeys])
.execute();
}
2024-04-23 20:11:07 -05:00
/** Collect event stats from the events. */
2024-04-23 19:31:40 -05:00
function gatherEventStats(events: DittoEvent[]): Promise<DittoTables['event_stats'][]> {
const ids = new Set<string>(
events
.filter((event) => event.kind === 1)
.map((event) => event.id),
);
if (!ids.size) {
return Promise.resolve([]);
}
2024-04-23 19:31:40 -05:00
return db
.selectFrom('event_stats')
.selectAll()
.where('event_id', 'in', [...ids])
.execute();
2024-03-30 17:44:17 -05:00
}
2024-01-24 15:46:45 -06:00
/** Return a normalized event without any non-standard keys. */
2024-03-16 13:19:39 -05:00
function purifyEvent(event: NostrEvent): NostrEvent {
2024-01-24 15:46:45 -06:00
return {
id: event.id,
pubkey: event.pubkey,
kind: event.kind,
content: event.content,
tags: event.tags,
sig: event.sig,
created_at: event.created_at,
};
}
export { hydrateEvents, purifyEvent };