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) const input = readSeriesInput({ tags, eventContent: event.content }) if (!input) { return null } const category = mapNostrCategoryToLegacy(tags.category) ?? 'science-fiction' const { hash, version, index } = await resolveObjectIdParts({ ...(input.idTag ? { idTag: input.idTag } : {}), defaultVersion: input.defaultVersion, defaultIndex: 0, generateHash: async (): Promise => generateHashId({ type: 'series', pubkey: event.pubkey, title: input.title, description: input.description, category: input.categoryTag, coverUrl: input.coverUrl, }), }) return buildSeriesFromParsed({ event, input, hash, version, index, category, }) } catch (e) { console.error('Error parsing series:', e) return null } } export async function parseReviewFromEvent(event: Event): Promise { try { const tags = extractTagsFromEvent(event) const input = readReviewInput(tags) if (!input) { return null } const { hash, version, index } = await resolveObjectIdParts({ ...(input.idTag ? { idTag: input.idTag } : {}), defaultVersion: input.defaultVersion, defaultIndex: 0, generateHash: async (): Promise => generateHashId({ type: 'quote', pubkey: event.pubkey, articleId: input.articleId, reviewerPubkey: input.reviewerPubkey, content: event.content, title: input.title ?? '', }), }) const rewardInfo = readRewardInfo(event.tags) const text = readTextTag(event.tags) return buildReviewFromParsed({ event, input, hash, version, index, rewardInfo, text }) } catch (e) { console.error('Error parsing review:', e) return null } } function buildSeriesFromParsed(params: { event: Event input: { title: string; description: string; preview: string; coverUrl: string } hash: string version: number index: number category: Series['category'] }): Series { const id = buildObjectId(params.hash, params.index, params.version) const series: Series = { id, hash: params.hash, version: params.version, index: params.index, pubkey: params.event.pubkey, title: params.input.title, description: params.input.description, preview: params.input.preview, thumbnailUrl: params.input.coverUrl, category: params.category, ...(params.input.coverUrl ? { coverUrl: params.input.coverUrl } : {}), } series.kindType = 'series' return series } function buildReviewFromParsed(params: { event: Event input: { articleId: string; reviewerPubkey: string; title: string | undefined; description: string } hash: string version: number index: number rewardInfo: { rewarded: boolean; rewardAmount: number | undefined } text: string | undefined }): Review { const id = buildObjectId(params.hash, params.index, params.version) const review: Review = { id, hash: params.hash, version: params.version, index: params.index, articleId: params.input.articleId, authorPubkey: params.event.pubkey, reviewerPubkey: params.input.reviewerPubkey, content: params.event.content, description: params.input.description, createdAt: params.event.created_at, ...(params.input.title ? { title: params.input.title } : {}), ...(params.text ? { text: params.text } : {}), ...(params.rewardInfo.rewarded ? { rewarded: true } : {}), ...(params.rewardInfo.rewardAmount !== undefined ? { rewardAmount: params.rewardInfo.rewardAmount } : {}), } review.kindType = 'review' return review } function readSeriesInput(params: { tags: ReturnType; eventContent: string }): { idTag: string | undefined defaultVersion: number title: string description: string preview: string coverUrl: string categoryTag: string } | null { if (params.tags.type !== 'series' || !params.tags.title || !params.tags.description) { return null } return { idTag: params.tags.id, defaultVersion: params.tags.version ?? 0, title: params.tags.title, description: params.tags.description, preview: params.tags.preview ?? params.eventContent.substring(0, 200), coverUrl: params.tags.coverUrl ?? '', categoryTag: params.tags.category ?? 'sciencefiction', } } function readReviewInput(tags: ReturnType): { idTag: string | undefined defaultVersion: number articleId: string reviewerPubkey: string title: string | undefined description: string } | null { if (tags.type !== 'quote' || !tags.articleId || !tags.reviewerPubkey) { return null } return { idTag: tags.id, defaultVersion: tags.version ?? 0, articleId: tags.articleId, reviewerPubkey: tags.reviewerPubkey, title: tags.title, description: tags.description ?? '', } } function readRewardInfo(eventTags: string[][]): { rewarded: boolean; rewardAmount: number | undefined } { const rewarded = eventTags.some((tag) => tag[0] === 'rewarded' && tag[1] === 'true') const rewardAmountTag = eventTags.find((tag) => tag[0] === 'reward_amount')?.[1] const rewardAmountRaw = rewardAmountTag ? parseInt(rewardAmountTag, 10) : NaN const rewardAmount = Number.isNaN(rewardAmountRaw) ? undefined : rewardAmountRaw return { rewarded, rewardAmount } } function readTextTag(eventTags: string[][]): string | undefined { return eventTags.find((tag) => tag[0] === 'text')?.[1] } 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 } }