375 lines
12 KiB
TypeScript
375 lines
12 KiB
TypeScript
import type { Event } from 'nostr-tools'
|
|
import type { Article, KindType, 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_<id>, #paywall, #payment
|
|
*/
|
|
export async function parseArticleFromEvent(event: Event): Promise<Article | null> {
|
|
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<Series | null> {
|
|
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
|
|
}
|
|
// Map category from new system to old system
|
|
let category: 'science-fiction' | 'scientific-research' = 'science-fiction'
|
|
if (tags.category === 'sciencefiction') {
|
|
category = 'science-fiction'
|
|
} else if (tags.category === 'research') {
|
|
category = 'scientific-research'
|
|
}
|
|
|
|
// 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 series data
|
|
hash = await 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<Review | null> {
|
|
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 }
|
|
}
|
|
|
|
async function buildArticle(event: Event, tags: ReturnType<typeof extractTagsFromEvent>, preview: string): Promise<Article> {
|
|
// Map category from new system to old system
|
|
const category = tags.category === 'sciencefiction' ? 'science-fiction' : tags.category === 'research' ? 'scientific-research' : undefined
|
|
const isPresentation = tags.type === 'author'
|
|
|
|
// 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)
|
|
if (parsed.hash) {
|
|
hash = parsed.hash
|
|
version = parsed.version ?? version
|
|
index = parsed.index ?? index
|
|
} else {
|
|
// If id is just a hash, use it directly
|
|
hash = tags.id
|
|
}
|
|
} else {
|
|
// Generate hash from article data
|
|
hash = await 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)
|
|
|
|
// Extract pages from JSON metadata if present
|
|
let pages: Page[] | undefined
|
|
try {
|
|
const jsonTag = event.tags.find((tag) => tag[0] === 'json')?.[1]
|
|
if (jsonTag) {
|
|
const metadata = JSON.parse(jsonTag) as { pages?: Page[] }
|
|
const { pages: metadataPages } = metadata
|
|
if (metadataPages && Array.isArray(metadataPages)) {
|
|
pages = metadataPages
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Ignore JSON parsing errors
|
|
}
|
|
|
|
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: typeof tags.bannerUrl === 'string' ? tags.bannerUrl : typeof tags.pictureUrl === 'string' ? tags.pictureUrl : '', // Required field with default
|
|
...(tags.invoice ? { invoice: tags.invoice } : {}),
|
|
...(tags.paymentHash ? { paymentHash: tags.paymentHash } : {}),
|
|
...(category ? { category } : {}),
|
|
...(isPresentation ? { isPresentation: true } : {}),
|
|
...(tags.mainnetAddress ? { mainnetAddress: tags.mainnetAddress } : {}),
|
|
...(tags.totalSponsoring ? { totalSponsoring: tags.totalSponsoring } : {}),
|
|
...(tags.seriesId ? { seriesId: tags.seriesId } : {}),
|
|
...(tags.bannerUrl ? { bannerUrl: tags.bannerUrl } : {}),
|
|
...(pages && pages.length > 0 ? { pages } : {}),
|
|
...(tags.type === 'publication' || tags.type === 'author' ? { kindType: 'article' as KindType } : {}),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse purchase from zap receipt event (kind 9735)
|
|
*/
|
|
export async function parsePurchaseFromEvent(event: Event): Promise<Purchase | null> {
|
|
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<Sponsoring | null> {
|
|
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<ReviewTip | null> {
|
|
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
|
|
}
|
|
}
|