joerglohrerde/snapshot/src/cli.ts

116 lines
3.9 KiB
TypeScript
Raw Normal View History

import { parseArgs } from '@std/cli'
import { join, resolve } from '@std/path'
import { loadConfig } from './core/config.ts'
import { loadReadRelays, fetchEvents } from './core/relays.ts'
import { dedupByDtag } from './core/dedup.ts'
import { filterDeleted } from './core/nip09-filter.ts'
import { runChecks } from './core/checks.ts'
import { buildPostJson } from './core/post-json.ts'
import { probeCover } from './core/cover-probe.ts'
import { writeOutput } from './core/output.ts'
import { readCache, writeCache, type CacheState } from './core/cache.ts'
import type { SignedEvent } from './core/types.ts'
async function main(): Promise<number> {
const args = parseArgs(Deno.args, {
string: ['out', 'cache', 'min-events'],
boolean: ['allow-shrink'],
default: {
out: resolve(import.meta.dirname!, '../output'),
},
})
const outDir = String(args.out)
const cachePath = args.cache ? String(args.cache) : join(outDir, '.last-snapshot.json')
const allowShrink = args['allow-shrink'] === true
const cfg = loadConfig()
const cache = await readCache(cachePath)
const minEvents = args['min-events']
? parseInt(String(args['min-events']), 10)
: cache
? Math.max(1, cache.lastKnownGoodCount - 2)
: 1
console.log('snapshot: bootstrap relay =', cfg.bootstrapRelay)
const readRelays = await loadReadRelays(cfg.bootstrapRelay, cfg.authorPubkeyHex)
console.log('snapshot: read relays =', readRelays.join(', '))
const fetched = await fetchEvents(readRelays, cfg.authorPubkeyHex)
console.log(
`snapshot: ${fetched.responded.length}/${fetched.queried.length} relays geantwortet, ` +
`${fetched.events.length} events roh`,
)
const posts: SignedEvent[] = []
const deletions: SignedEvent[] = []
for (const ev of fetched.events) {
if (ev.kind === 30023) posts.push(ev)
else if (ev.kind === 5) deletions.push(ev)
}
const dedupedPosts = dedupByDtag(posts)
const filtered = filterDeleted(dedupedPosts, deletions, cfg.authorPubkeyHex)
const previousDeletedCoords = new Set(cache?.deletedCoords ?? [])
const newlyDeletedCount = deletions.flatMap((d) =>
d.tags.filter((t) => t[0] === 'a' && t[1] && !previousDeletedCoords.has(t[1])).map((t) => t[1])
).length
runChecks({
relaysQueried: fetched.queried.length,
relaysResponded: fetched.responded.length,
eventCount: filtered.length,
minEvents,
lastKnownGoodCount: cache?.lastKnownGoodCount,
newDeletionsCount: newlyDeletedCount,
allowShrink,
})
const titleByDtag = new Map<string, string>()
for (const ev of filtered) {
const d = ev.tags.find((t) => t[0] === 'd')?.[1]
const title = ev.tags.find((t) => t[0] === 'title')?.[1]
if (d && title) titleByDtag.set(d, title)
}
const postJsons = filtered.map((ev) => buildPostJson(ev, titleByDtag))
for (const p of postJsons) {
if (!p.cover_image) continue
const probe = await probeCover(p.cover_image.url)
if (!probe.reachable) {
console.warn(
`snapshot: cover unreachable [${probe.status}] ${p.cover_image.url} (slug=${p.slug}) — URL wird trotzdem geschrieben`,
)
}
}
await writeOutput(outDir, {
generatedAt: new Date().toISOString(),
authorPubkey: cfg.authorPubkeyHex,
relaysQueried: fetched.queried,
relaysResponded: fetched.responded,
posts: postJsons,
})
const allDeletedCoords = deletions.flatMap((d) =>
d.tags.filter((t) => t[0] === 'a' && t[1]).map((t) => t[1] as string)
)
const newCache: CacheState = {
lastKnownGoodCount: filtered.length,
deletedCoords: [...new Set(allDeletedCoords)],
}
await writeCache(cachePath, newCache)
console.log(`snapshot: ${filtered.length} posts geschrieben nach ${outDir}`)
return 0
}
if (import.meta.main) {
try {
Deno.exit(await main())
} catch (err) {
console.error('snapshot: HARD-FAIL —', err instanceof Error ? err.message : String(err))
Deno.exit(1)
}
}