Expand ecash explorer capabilities
Add Fedimint discovery, dual SQLite/Postgres storage, richer review handling, and generated social imagery.
This commit is contained in:
+27
-31
@@ -15,14 +15,12 @@ import {
|
||||
KIND_REVIEW,
|
||||
PROFILE_RELAYS,
|
||||
cleanReviewContent,
|
||||
isCashuMintReview,
|
||||
normalizeMintUrl,
|
||||
mintUrlsFromEvent,
|
||||
parseProfileContent,
|
||||
parseRating,
|
||||
WRITTEN_MIN_CHARS,
|
||||
type Profile,
|
||||
} from '@cashumints/shared';
|
||||
import { buildFeedIndex, resolveFeedTarget, type FeedTarget } from './feed-resolve';
|
||||
|
||||
const list = (value: string | undefined): string[] | null => {
|
||||
const parsed = value?.split(',').map((relay) => relay.trim()).filter(Boolean);
|
||||
@@ -47,6 +45,8 @@ export interface BuildReview {
|
||||
content: string;
|
||||
mint_url: string;
|
||||
mint_host: string;
|
||||
/** `cashu` or `fedimint`: which route the card links to, and which filter shows it. */
|
||||
mint_type: string;
|
||||
profile: Profile | null;
|
||||
}
|
||||
|
||||
@@ -186,15 +186,19 @@ function readReviewEvents(): Promise<NostrEvent[]> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Newest review events that are worth showing, resolved against the mints the API
|
||||
* knows about. `hostByUrl` maps normalized mint URL to routing slug.
|
||||
* Newest review events that are worth showing, resolved against everything the API
|
||||
* knows about — mints and federations both, since the strip's heading is "latest
|
||||
* reviews" and not "latest mint reviews". In practice federations rarely reach it: the
|
||||
* junk filter above wants a written body, and almost every 38173 review on the network
|
||||
* is a bare `[5/5]`.
|
||||
*/
|
||||
export async function fetchLatestReviews(
|
||||
hostByUrl: Map<string, string>,
|
||||
targets: readonly FeedTarget[],
|
||||
limit: number,
|
||||
): Promise<BuildReview[]> {
|
||||
try {
|
||||
const events = await readReviewEvents();
|
||||
const index = buildFeedIndex(targets);
|
||||
|
||||
const candidates: BuildReview[] = [];
|
||||
const seenAuthors = new Set<string>();
|
||||
@@ -202,15 +206,9 @@ export async function fetchLatestReviews(
|
||||
for (const event of [...events].sort((a, b) => b.created_at - a.created_at)) {
|
||||
const content = cleanReviewContent(event.content);
|
||||
if (!qualifies(content)) continue;
|
||||
if (!isCashuMintReview(event) && mintUrlsFromEvent(event).length === 0) continue;
|
||||
|
||||
const raw = mintUrlsFromEvent(event)[0];
|
||||
if (!raw) continue;
|
||||
const normalized = normalizeMintUrl(raw);
|
||||
if (!normalized) continue;
|
||||
|
||||
const host = hostByUrl.get(normalized.url);
|
||||
if (!host) continue;
|
||||
const target = resolveFeedTarget(event, index);
|
||||
if (!target) continue;
|
||||
|
||||
// One card per author, so the strip is not three posts from the same npub.
|
||||
if (seenAuthors.has(event.pubkey)) continue;
|
||||
@@ -223,8 +221,9 @@ export async function fetchLatestReviews(
|
||||
created_at: event.created_at,
|
||||
rating: parseRating(event),
|
||||
content,
|
||||
mint_url: normalized.url,
|
||||
mint_host: host,
|
||||
mint_url: target.url,
|
||||
mint_host: target.host,
|
||||
mint_type: target.type,
|
||||
profile: null,
|
||||
});
|
||||
|
||||
@@ -261,7 +260,7 @@ export async function fetchLatestReviews(
|
||||
}
|
||||
|
||||
/**
|
||||
* The global feed: newest written reviews across every mint.
|
||||
* The global feed: newest written reviews across every mint and federation.
|
||||
*
|
||||
* Same source and same parsing as the home page strip, three differences:
|
||||
* every author may appear more than once (once per mint they reviewed), the junk
|
||||
@@ -275,33 +274,29 @@ export async function fetchLatestReviews(
|
||||
* visitor, only for a crawler.
|
||||
*/
|
||||
export async function fetchReviewFeed(
|
||||
hostByUrl: Map<string, string>,
|
||||
targets: readonly FeedTarget[],
|
||||
limit: number,
|
||||
): Promise<BuildReview[]> {
|
||||
try {
|
||||
const events = await readReviewEvents();
|
||||
const index = buildFeedIndex(targets);
|
||||
|
||||
/*
|
||||
* Kind 38000 is addressable: a later event from the same author about the same
|
||||
* mint replaces the earlier one, exactly as the mint page treats it. Keyed on
|
||||
* author plus mint, so one person reviewing three mints keeps three reviews.
|
||||
* subject replaces the earlier one, exactly as a subject page treats it. Keyed on
|
||||
* author plus subject, so one person reviewing three mints and a federation keeps
|
||||
* four reviews.
|
||||
*/
|
||||
const newest = new Map<string, BuildReview>();
|
||||
|
||||
for (const event of events) {
|
||||
const content = cleanReviewContent(event.content);
|
||||
if (content.length < WRITTEN_MIN_CHARS) continue;
|
||||
if (!isCashuMintReview(event) && mintUrlsFromEvent(event).length === 0) continue;
|
||||
|
||||
const raw = mintUrlsFromEvent(event)[0];
|
||||
if (!raw) continue;
|
||||
const normalized = normalizeMintUrl(raw);
|
||||
if (!normalized) continue;
|
||||
const target = resolveFeedTarget(event, index);
|
||||
if (!target) continue;
|
||||
|
||||
const host = hostByUrl.get(normalized.url);
|
||||
if (!host) continue;
|
||||
|
||||
const key = `${event.pubkey}:${host}`;
|
||||
const key = `${event.pubkey}:${target.host}`;
|
||||
const existing = newest.get(key);
|
||||
if (existing && existing.created_at >= event.created_at) continue;
|
||||
|
||||
@@ -312,8 +307,9 @@ export async function fetchReviewFeed(
|
||||
created_at: event.created_at,
|
||||
rating: parseRating(event),
|
||||
content,
|
||||
mint_url: normalized.url,
|
||||
mint_host: host,
|
||||
mint_url: target.url,
|
||||
mint_host: target.host,
|
||||
mint_type: target.type,
|
||||
profile: null,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user