You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
148 lines
4.6 KiB
148 lines
4.6 KiB
import { isLikelyCachedNostrEvent } from '@/services/indexed-db.service' |
|
import type { Event } from 'nostr-tools' |
|
import { validateEvent, verifyEvent } from 'nostr-tools' |
|
|
|
/** Max size for a pasted JSON blob (single event or small array). */ |
|
export const CACHE_IMPORT_MAX_PASTE_CHARS = 400_000 |
|
|
|
/** Max `.jsonl` upload size. */ |
|
export const CACHE_IMPORT_MAX_JSONL_FILE_BYTES = 2 * 1024 * 1024 |
|
|
|
/** Max lines processed from a `.jsonl` file. */ |
|
export const CACHE_IMPORT_MAX_JSONL_LINES = 500 |
|
|
|
/** Skip oversized single lines in JSONL. */ |
|
export const CACHE_IMPORT_MAX_JSONL_LINE_CHARS = 256_000 |
|
|
|
export type CacheImportParseIssue = { line?: number; message: string } |
|
|
|
export type CacheImportParseResult = { |
|
events: Event[] |
|
issues: CacheImportParseIssue[] |
|
} |
|
|
|
function normalizeImportedEvent(raw: unknown): Event | null { |
|
if (!isLikelyCachedNostrEvent(raw)) return null |
|
const ev = raw as Event |
|
if (typeof ev.created_at !== 'number' || !Number.isFinite(ev.created_at)) return null |
|
if (typeof ev.sig !== 'string' || !ev.sig.trim()) return null |
|
if (!validateEvent(ev)) return null |
|
if (!verifyEvent(ev)) return null |
|
return ev |
|
} |
|
|
|
function collectFromUnknown( |
|
value: unknown, |
|
line: number | undefined, |
|
events: Event[], |
|
issues: CacheImportParseIssue[] |
|
): void { |
|
if (Array.isArray(value)) { |
|
if (value.length + events.length > CACHE_IMPORT_MAX_JSONL_LINES) { |
|
issues.push({ |
|
line, |
|
message: `Too many events (max ${CACHE_IMPORT_MAX_JSONL_LINES})` |
|
}) |
|
return |
|
} |
|
for (let i = 0; i < value.length; i++) { |
|
collectFromUnknown(value[i], line, events, issues) |
|
if (events.length >= CACHE_IMPORT_MAX_JSONL_LINES) break |
|
} |
|
return |
|
} |
|
const ev = normalizeImportedEvent(value) |
|
if (ev) { |
|
events.push(ev) |
|
return |
|
} |
|
issues.push({ |
|
line, |
|
message: line != null ? 'Invalid or unsigned event on this line' : 'Invalid or unsigned Nostr event JSON' |
|
}) |
|
} |
|
|
|
/** Parse one pasted JSON value (object or array of events). */ |
|
export function parsePastedCacheImportJson(text: string): CacheImportParseResult { |
|
const trimmed = text.trim() |
|
if (!trimmed) { |
|
return { events: [], issues: [{ message: 'Paste is empty' }] } |
|
} |
|
if (trimmed.length > CACHE_IMPORT_MAX_PASTE_CHARS) { |
|
return { |
|
events: [], |
|
issues: [ |
|
{ |
|
message: `Paste exceeds ${Math.round(CACHE_IMPORT_MAX_PASTE_CHARS / 1024)} KB limit` |
|
} |
|
] |
|
} |
|
} |
|
let parsed: unknown |
|
try { |
|
parsed = JSON.parse(trimmed) as unknown |
|
} catch { |
|
return { events: [], issues: [{ message: 'Invalid JSON' }] } |
|
} |
|
const events: Event[] = [] |
|
const issues: CacheImportParseIssue[] = [] |
|
collectFromUnknown(parsed, undefined, events, issues) |
|
return { events, issues } |
|
} |
|
|
|
/** Parse newline-delimited JSON (one event per line). */ |
|
export function parseJsonlCacheImportText(text: string): CacheImportParseResult { |
|
if (text.length > CACHE_IMPORT_MAX_JSONL_FILE_BYTES) { |
|
return { |
|
events: [], |
|
issues: [ |
|
{ |
|
message: `File content exceeds ${Math.round(CACHE_IMPORT_MAX_JSONL_FILE_BYTES / (1024 * 1024))} MB limit` |
|
} |
|
] |
|
} |
|
} |
|
const lines = text.split(/\r?\n/) |
|
const events: Event[] = [] |
|
const issues: CacheImportParseIssue[] = [] |
|
let nonEmptyLines = 0 |
|
for (let i = 0; i < lines.length; i++) { |
|
const line = lines[i].trim() |
|
if (!line) continue |
|
nonEmptyLines++ |
|
if (nonEmptyLines > CACHE_IMPORT_MAX_JSONL_LINES) { |
|
issues.push({ |
|
line: i + 1, |
|
message: `Stopped at ${CACHE_IMPORT_MAX_JSONL_LINES} events (file has more lines)` |
|
}) |
|
break |
|
} |
|
if (line.length > CACHE_IMPORT_MAX_JSONL_LINE_CHARS) { |
|
issues.push({ line: i + 1, message: 'Line too large' }) |
|
continue |
|
} |
|
let parsed: unknown |
|
try { |
|
parsed = JSON.parse(line) as unknown |
|
} catch { |
|
issues.push({ line: i + 1, message: 'Invalid JSON on this line' }) |
|
continue |
|
} |
|
const before = events.length |
|
collectFromUnknown(parsed, i + 1, events, issues) |
|
if (events.length === before && issues[issues.length - 1]?.line !== i + 1) { |
|
issues.push({ line: i + 1, message: 'Invalid or unsigned event on this line' }) |
|
} |
|
if (events.length >= CACHE_IMPORT_MAX_JSONL_LINES) break |
|
} |
|
if (nonEmptyLines === 0) { |
|
issues.push({ message: 'File has no event lines' }) |
|
} |
|
return { events, issues } |
|
} |
|
|
|
export function formatCacheImportLimits(): string { |
|
const mb = Math.round(CACHE_IMPORT_MAX_JSONL_FILE_BYTES / (1024 * 1024)) |
|
const pasteKb = Math.round(CACHE_IMPORT_MAX_PASTE_CHARS / 1024) |
|
return `${CACHE_IMPORT_MAX_JSONL_LINES} events max · ${mb} MB file · ${pasteKb} KB paste` |
|
}
|
|
|