import type { Event } from 'nostr-tools' import type { Article, Page, Purchase, Review, ReviewTip, Series, Sponsoring } from '@/types/nostr' import { extractTagsFromEvent } from './nostrTagSystem' import { buildObjectId, parseObjectId } from './urlGenerator' import { generateHashId } from './hashIdGenerator' import { extractPurchaseFromEvent, extractReviewTipFromEvent, extractSponsoringFromEvent } from './metadataExtractor' /** * Parse article metadata from Nostr event * Uses new tag system: #publication, #sciencefiction|research, #id_, #paywall, #payment */ export async function parseArticleFromEvent(event: Event): Promise
{ try { const tags = extractTagsFromEvent(event) // Check if it's a publication type if (tags.type !== 'publication') { return null } const { previewContent } = getPreviewContent(event.content, tags.preview) return buildArticle(event, tags, previewContent) } catch (e) { console.error('Error parsing article:', e) return null } } export async function parseSeriesFromEvent(event: Event): Promise { try { const tags = extractTagsFromEvent(event) // Check if it's a series type (tag is 'series' in English) if (tags.type !== 'series') { return null } if (!tags.title || !tags.description) { return null } const category = mapNostrCategoryToLegacy(tags.category) ?? 'science-fiction' const { hash, version, index } = await resolveObjectIdParts({ ...(tags.id ? { idTag: tags.id } : {}), defaultVersion: tags.version ?? 0, defaultIndex: 0, generateHash: async (): Promise => generateHashId({ type: 'series', pubkey: event.pubkey, title: tags.title, description: tags.description, category: tags.category ?? 'sciencefiction', coverUrl: tags.coverUrl ?? '', }), }) const id = buildObjectId(hash, index, version) const series: Series = { id, hash, version, index, pubkey: event.pubkey, title: tags.title, description: tags.description, preview: (tags.preview) ?? event.content.substring(0, 200), thumbnailUrl: tags.coverUrl ?? '', // Use coverUrl as thumbnail if available category, ...(tags.coverUrl ? { coverUrl: tags.coverUrl } : {}), } series.kindType = 'series' return series } catch (e) { console.error('Error parsing series:', e) return null } } export async function parseReviewFromEvent(event: Event): Promise { try { const tags = extractTagsFromEvent(event) // Check if it's a quote type (reviews are quotes, tag is 'quote' in English) if (tags.type !== 'quote') { return null } const {articleId} = tags const reviewer = tags.reviewerPubkey if (!articleId || !reviewer) { return null } const rewardedTag = event.tags.find((tag) => tag[0] === 'rewarded' && tag[1] === 'true') const rewardAmountTag = event.tags.find((tag) => tag[0] === 'reward_amount') // Extract hash, version, index from id tag or parse it let hash: string let version = tags.version ?? 0 let index = 0 if (tags.id) { const parsed = parseObjectId(tags.id) const { hash: parsedHash, version: parsedVersion, index: parsedIndex } = parsed if (parsedHash) { hash = parsedHash version = parsedVersion ?? version index = parsedIndex ?? index } else { // If id is just a hash, use it directly hash = tags.id } } else { // Generate hash from review data hash = await generateHashId({ type: 'quote', pubkey: event.pubkey, articleId, reviewerPubkey: reviewer, content: event.content, title: tags.title ?? '', }) } const id = buildObjectId(hash, index, version) // Extract text from tags if present const textTag = event.tags.find((tag) => tag[0] === 'text')?.[1] const review: Review = { id, hash, version, index, articleId, authorPubkey: event.pubkey, reviewerPubkey: reviewer, content: event.content, description: tags.description ?? '', // Required field with default createdAt: event.created_at, ...(tags.title ? { title: tags.title } : {}), ...(textTag ? { text: textTag } : {}), ...(rewardedTag ? { rewarded: true } : {}), ...(rewardAmountTag ? { rewardAmount: parseInt(rewardAmountTag[1] ?? '0', 10) } : {}), } review.kindType = 'review' return review } catch (e) { console.error('Error parsing review:', e) return null } } function getPreviewContent(content: string, previewTag?: string): { previewContent: string } { const lines = content.split('\n') const previewContent = previewTag ?? lines[0] ?? content.substring(0, 200) return { previewContent } } function mapNostrCategoryToLegacy( category: string | undefined ): 'science-fiction' | 'scientific-research' | undefined { if (category === 'sciencefiction') { return 'science-fiction' } if (category === 'research') { return 'scientific-research' } return undefined } interface ObjectIdParts { hash: string version: number index: number } function parseObjectIdPartsFromTag(params: { idTag: string; defaultVersion: number; defaultIndex: number }): ObjectIdParts { const parsed = parseObjectId(params.idTag) const hash = parsed.hash ?? params.idTag const version = parsed.version ?? params.defaultVersion const index = parsed.index ?? params.defaultIndex return { hash, version, index } } async function resolveObjectIdParts(params: { idTag?: string defaultVersion: number defaultIndex: number generateHash: () => Promise }): Promise { if (params.idTag) { return parseObjectIdPartsFromTag({ idTag: params.idTag, defaultVersion: params.defaultVersion, defaultIndex: params.defaultIndex, }) } const hash = await params.generateHash() return { hash, version: params.defaultVersion, index: params.defaultIndex } } function resolveThumbnailUrl(tags: ReturnType): string { if (typeof tags.bannerUrl === 'string') { return tags.bannerUrl } if (typeof tags.pictureUrl === 'string') { return tags.pictureUrl } return '' } function parsePagesFromEventJsonTag(event: Event): Page[] | undefined { const jsonTag = event.tags.find((tag) => tag[0] === 'json')?.[1] if (!jsonTag) { return undefined } try { const parsed: unknown = JSON.parse(jsonTag) if (typeof parsed !== 'object' || parsed === null) { return undefined } const metadata = parsed as { pages?: unknown } if (!Array.isArray(metadata.pages)) { return undefined } return metadata.pages as Page[] } catch { return undefined } } function buildArticlePaymentFields(tags: ReturnType): Partial
{ const result: Partial
= {} if (tags.invoice) { result.invoice = tags.invoice } if (tags.paymentHash) { result.paymentHash = tags.paymentHash } return result } function buildArticleClassificationFields(params: { tags: ReturnType category: 'science-fiction' | 'scientific-research' | undefined isPresentation: boolean }): Partial
{ const result: Partial
= {} if (params.category) { result.category = params.category } if (params.isPresentation) { result.isPresentation = true } if (params.tags.type === 'publication' || params.tags.type === 'author') { result.kindType = 'article' } return result } function buildArticleOptionalMetaFields(params: { tags: ReturnType pages: Page[] | undefined }): Partial
{ const result: Partial
= {} if (params.tags.mainnetAddress) { result.mainnetAddress = params.tags.mainnetAddress } if (params.tags.totalSponsoring) { result.totalSponsoring = params.tags.totalSponsoring } if (params.tags.seriesId) { result.seriesId = params.tags.seriesId } if (params.tags.bannerUrl) { result.bannerUrl = params.tags.bannerUrl } if (params.pages && params.pages.length > 0) { result.pages = params.pages } return result } async function buildArticle(event: Event, tags: ReturnType, preview: string): Promise
{ const category = mapNostrCategoryToLegacy(tags.category) const isPresentation = tags.type === 'author' const { hash, version, index } = await resolveObjectIdParts({ ...(tags.id ? { idTag: tags.id } : {}), defaultVersion: tags.version ?? 0, defaultIndex: 0, generateHash: async (): Promise => generateHashId({ type: isPresentation ? 'author' : 'publication', pubkey: event.pubkey, title: tags.title ?? 'Untitled', preview, category: tags.category ?? 'sciencefiction', seriesId: tags.seriesId ?? '', bannerUrl: tags.bannerUrl ?? '', zapAmount: tags.zapAmount ?? 800, }), }) const id = buildObjectId(hash, index, version) const pages = parsePagesFromEventJsonTag(event) return { id, hash, version, index, pubkey: event.pubkey, title: tags.title ?? 'Untitled', preview, content: '', description: tags.description ?? '', // Required field with default contentDescription: tags.description ?? '', // Required field with default (can be improved later) createdAt: event.created_at, zapAmount: tags.zapAmount ?? 800, paid: false, thumbnailUrl: resolveThumbnailUrl(tags), // Required field with default ...buildArticlePaymentFields(tags), ...buildArticleClassificationFields({ tags, category, isPresentation }), ...buildArticleOptionalMetaFields({ tags, pages }), } } /** * Parse purchase from zap receipt event (kind 9735) */ export async function parsePurchaseFromEvent(event: Event): Promise { try { const extracted = await extractPurchaseFromEvent(event) if (!extracted) { return null } // Extract hash, version, index from id const parsed = parseObjectId(extracted.id) const { hash: parsedHash, version: parsedVersion, index: parsedIndex } = parsed const hash = parsedHash ?? extracted.id const version = parsedVersion ?? 0 const index = parsedIndex ?? 0 const id = buildObjectId(hash, index, version) return { id, hash, version, index, payerPubkey: extracted.payerPubkey, articleId: extracted.articleId, authorPubkey: extracted.authorPubkey, amount: extracted.amount, paymentHash: extracted.paymentHash, createdAt: event.created_at, kindType: 'purchase', } } catch (e) { console.error('Error parsing purchase:', e) return null } } /** * Parse sponsoring from zap receipt event (kind 9735) */ export async function parseSponsoringFromEvent(event: Event): Promise { try { const extracted = await extractSponsoringFromEvent(event) if (!extracted) { return null } // Extract hash, version, index from id const parsed = parseObjectId(extracted.id) const { hash: parsedHash, version: parsedVersion, index: parsedIndex } = parsed const hash = parsedHash ?? extracted.id const version = parsedVersion ?? 0 const index = parsedIndex ?? 0 const id = buildObjectId(hash, index, version) // Extract text from tags if present const textTag = event.tags.find((tag) => tag[0] === 'text')?.[1] return { id, hash, version, index, payerPubkey: extracted.payerPubkey, authorPubkey: extracted.authorPubkey, amount: extracted.amount, paymentHash: extracted.paymentHash, createdAt: event.created_at, ...(extracted.seriesId ? { seriesId: extracted.seriesId } : {}), ...(extracted.articleId ? { articleId: extracted.articleId } : {}), ...(textTag ? { text: textTag } : {}), kindType: 'sponsoring', } } catch (e) { console.error('Error parsing sponsoring:', e) return null } } /** * Parse review tip from zap receipt event (kind 9735) */ export async function parseReviewTipFromEvent(event: Event): Promise { try { const extracted = await extractReviewTipFromEvent(event) if (!extracted) { return null } // Extract hash, version, index from id const parsed = parseObjectId(extracted.id) const { hash: parsedHash, version: parsedVersion, index: parsedIndex } = parsed const hash = parsedHash ?? extracted.id const version = parsedVersion ?? 0 const index = parsedIndex ?? 0 const id = buildObjectId(hash, index, version) // Extract text from tags if present const textTag = event.tags.find((tag) => tag[0] === 'text')?.[1] return { id, hash, version, index, payerPubkey: extracted.payerPubkey, articleId: extracted.articleId, reviewId: extracted.reviewId, reviewerPubkey: extracted.reviewerPubkey, authorPubkey: extracted.authorPubkey, amount: extracted.amount, paymentHash: extracted.paymentHash, createdAt: event.created_at, ...(textTag ? { text: textTag } : {}), kindType: 'review_tip', } } catch (e) { console.error('Error parsing review tip:', e) return null } }