2024-04-19 16:39:35 -05:00
|
|
|
import { NostrEvent, NStore } from '@nostrify/nostrify';
|
2024-03-30 17:44:17 -05:00
|
|
|
import { db } from '@/db.ts';
|
2024-01-23 12:07:22 -06:00
|
|
|
import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
|
2024-01-04 01:44:56 -06:00
|
|
|
|
2024-01-23 12:07:22 -06:00
|
|
|
interface HydrateEventOpts {
|
|
|
|
events: DittoEvent[];
|
2024-01-23 14:06:16 -06:00
|
|
|
storage: NStore;
|
2024-01-04 02:09:23 -06:00
|
|
|
signal?: AbortSignal;
|
2024-01-04 01:44:56 -06:00
|
|
|
}
|
|
|
|
|
2024-04-22 19:51:29 -03:00
|
|
|
/** Hydrate events using the provided storage. */
|
2024-01-23 12:07:22 -06:00
|
|
|
async function hydrateEvents(opts: HydrateEventOpts): Promise<DittoEvent[]> {
|
2024-04-22 19:51:29 -03:00
|
|
|
const { events, storage, signal } = opts;
|
2024-01-04 01:44:56 -06:00
|
|
|
|
2024-04-22 19:51:29 -03:00
|
|
|
if (!events.length) {
|
2024-03-05 14:26:38 -06:00
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
2024-04-23 15:56:35 -03:00
|
|
|
const allEventsMap: Map<string, DittoEvent> = new Map(events.map((event) => {
|
|
|
|
return [event.id, structuredClone(event)];
|
|
|
|
}));
|
2024-04-22 19:51:29 -03:00
|
|
|
|
|
|
|
const childrenEventsIds = (events.map((event) => {
|
|
|
|
if (event.kind === 1) return event.tags.find(([name]) => name === 'q')?.[1]; // possible quote repost
|
|
|
|
if (event.kind === 6) return event.tags.find(([name]) => name === 'e')?.[1]; // possible repost
|
|
|
|
return;
|
|
|
|
}).filter(Boolean)) as string[];
|
|
|
|
|
|
|
|
if (childrenEventsIds.length > 0) {
|
|
|
|
const childrenEvents = await storage.query([{ ids: childrenEventsIds }], { signal });
|
2024-04-23 15:56:35 -03:00
|
|
|
childrenEvents.forEach((event) => {
|
|
|
|
allEventsMap.set(event.id, structuredClone(event));
|
|
|
|
});
|
2024-04-22 19:51:29 -03:00
|
|
|
|
|
|
|
if (childrenEvents.length > 0) {
|
|
|
|
const grandChildrenEventsIds = (childrenEvents.map((event) => {
|
|
|
|
if (event.kind === 1) return event.tags.find(([name]) => name === 'q')?.[1]; // possible quote repost
|
|
|
|
return;
|
|
|
|
}).filter(Boolean)) as string[];
|
|
|
|
if (grandChildrenEventsIds.length > 0) {
|
|
|
|
const grandChildrenEvents = await storage.query([{ ids: grandChildrenEventsIds }], { signal });
|
2024-04-23 15:56:35 -03:00
|
|
|
grandChildrenEvents.forEach((event) => {
|
|
|
|
allEventsMap.set(event.id, structuredClone(event));
|
|
|
|
});
|
2024-04-22 19:51:29 -03:00
|
|
|
}
|
2024-01-04 01:44:56 -06:00
|
|
|
}
|
|
|
|
}
|
2024-04-23 15:56:35 -03:00
|
|
|
await hydrateAuthors({ events: [...allEventsMap.values()], storage, signal });
|
2024-04-23 17:16:15 -03:00
|
|
|
await hydrateAuthorStats([...allEventsMap.values()].filter((e) => e.kind === 0));
|
|
|
|
await hydrateEventStats([...allEventsMap.values()].filter((e) => e.kind === 1));
|
2024-04-22 19:51:29 -03:00
|
|
|
|
|
|
|
events.forEach((event) => {
|
2024-04-23 15:56:35 -03:00
|
|
|
const correspondingEvent = allEventsMap.get(event.id);
|
2024-04-22 19:51:29 -03:00
|
|
|
if (correspondingEvent?.author) event.author = correspondingEvent.author;
|
|
|
|
if (correspondingEvent?.author_stats) event.author_stats = correspondingEvent.author_stats;
|
|
|
|
if (correspondingEvent?.event_stats) event.event_stats = correspondingEvent.event_stats;
|
|
|
|
|
|
|
|
if (event.kind === 1) {
|
|
|
|
const quoteId = event.tags.find(([name]) => name === 'q')?.[1];
|
|
|
|
if (quoteId) {
|
2024-04-23 15:56:35 -03:00
|
|
|
event.quote_repost = allEventsMap.get(quoteId);
|
2024-04-22 19:51:29 -03:00
|
|
|
}
|
|
|
|
} else if (event.kind === 6) {
|
|
|
|
const repostedId = event.tags.find(([name]) => name === 'e')?.[1];
|
|
|
|
if (repostedId) {
|
2024-04-23 15:56:35 -03:00
|
|
|
const repostedEvent = allEventsMap.get(repostedId);
|
2024-04-22 19:51:29 -03:00
|
|
|
if (repostedEvent && repostedEvent.tags.find(([name]) => name === 'q')?.[1]) { // The repost is a repost of a quote repost
|
|
|
|
const postBeingQuoteRepostedId = repostedEvent.tags.find(([name]) => name === 'q')?.[1];
|
|
|
|
event.repost = {
|
2024-04-23 15:56:35 -03:00
|
|
|
quote_repost: allEventsMap.get(postBeingQuoteRepostedId!),
|
|
|
|
...allEventsMap.get(repostedId)!,
|
2024-04-22 19:51:29 -03:00
|
|
|
};
|
|
|
|
} else { // The repost is a repost of a normal post
|
2024-04-23 15:56:35 -03:00
|
|
|
event.repost = allEventsMap.get(repostedId);
|
2024-04-22 19:51:29 -03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2024-01-04 01:44:56 -06:00
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
2024-03-30 16:12:48 -05:00
|
|
|
async function hydrateAuthors(opts: Omit<HydrateEventOpts, 'relations'>): Promise<DittoEvent[]> {
|
|
|
|
const { events, storage, signal } = opts;
|
|
|
|
|
|
|
|
const pubkeys = new Set([...events].map((event) => event.pubkey));
|
|
|
|
const authors = await storage.query([{ kinds: [0], authors: [...pubkeys], limit: pubkeys.size }], { signal });
|
|
|
|
|
|
|
|
for (const event of events) {
|
|
|
|
event.author = authors.find((author) => author.pubkey === event.pubkey);
|
|
|
|
}
|
|
|
|
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
2024-03-30 17:44:17 -05:00
|
|
|
async function hydrateAuthorStats(events: DittoEvent[]): Promise<DittoEvent[]> {
|
|
|
|
const results = await db
|
|
|
|
.selectFrom('author_stats')
|
|
|
|
.selectAll()
|
|
|
|
.where('pubkey', 'in', events.map((event) => event.pubkey))
|
|
|
|
.execute();
|
|
|
|
|
|
|
|
for (const event of events) {
|
|
|
|
const stat = results.find((result) => result.pubkey === event.pubkey);
|
|
|
|
if (stat) {
|
|
|
|
event.author_stats = {
|
|
|
|
followers_count: Math.max(stat.followers_count, 0) || 0,
|
|
|
|
following_count: Math.max(stat.following_count, 0) || 0,
|
|
|
|
notes_count: Math.max(stat.notes_count, 0) || 0,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function hydrateEventStats(events: DittoEvent[]): Promise<DittoEvent[]> {
|
|
|
|
const results = await db
|
|
|
|
.selectFrom('event_stats')
|
|
|
|
.selectAll()
|
|
|
|
.where('event_id', 'in', events.map((event) => event.id))
|
|
|
|
.execute();
|
|
|
|
|
|
|
|
for (const event of events) {
|
|
|
|
const stat = results.find((result) => result.event_id === event.id);
|
|
|
|
if (stat) {
|
|
|
|
event.event_stats = {
|
|
|
|
replies_count: Math.max(stat.replies_count, 0) || 0,
|
|
|
|
reposts_count: Math.max(stat.reposts_count, 0) || 0,
|
|
|
|
reactions_count: Math.max(stat.reactions_count, 0) || 0,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return events;
|
|
|
|
}
|
|
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-03-16 13:19:11 -05:00
|
|
|
export { hydrateEvents, purifyEvent };
|