story-research-zapwall/lib/articleMutations.ts

249 lines
7.8 KiB
TypeScript

import { nostrService } from './nostr'
import { createArticleInvoice, createPreviewEvent } from './articleInvoice'
import { storePrivateContent, getStoredPrivateContent } from './articleStorage'
import { buildTags } from './nostrTagSystem'
import type { ArticleDraft, PublishedArticle } from './articlePublisher'
import type { AlbyInvoice } from '@/types/alby'
import type { Review, Series } from '@/types/nostr'
export interface ArticleUpdateResult extends PublishedArticle {
originalArticleId: string
}
function ensureKeys(authorPubkey: string, authorPrivateKey?: string): void {
nostrService.setPublicKey(authorPubkey)
if (authorPrivateKey) {
nostrService.setPrivateKey(authorPrivateKey)
} else if (!nostrService.getPrivateKey()) {
throw new Error('Private key required for signing. Connect a wallet that can sign.')
}
}
function requireCategory(category?: ArticleDraft['category']): asserts category is NonNullable<ArticleDraft['category']> {
if (category !== 'science-fiction' && category !== 'scientific-research') {
throw new Error('Vous devez sélectionner une catégorie (science-fiction ou recherche scientifique).')
}
}
async function ensurePresentation(authorPubkey: string): Promise<string> {
const presentation = await articlePublisher.getAuthorPresentation(authorPubkey)
if (!presentation) {
throw new Error('Vous devez créer un article de présentation avant de publier des articles.')
}
return presentation.id
}
async function publishPreviewWithInvoice(
draft: ArticleDraft,
invoice: AlbyInvoice,
presentationId: string,
extraTags?: string[][]
): Promise<import('nostr-tools').Event | null> {
const previewEvent = createPreviewEvent(draft, invoice, presentationId, extraTags)
const publishedEvent = await nostrService.publishEvent(previewEvent)
return publishedEvent ?? null
}
export async function publishSeries(params: {
title: string
description: string
preview?: string
coverUrl?: string
category: ArticleDraft['category']
authorPubkey: string
authorPrivateKey?: string
}): Promise<Series> {
ensureKeys(params.authorPubkey, params.authorPrivateKey)
const category = params.category
requireCategory(category)
const event = buildSeriesEvent(params, category)
const published = await nostrService.publishEvent(event)
if (!published) {
throw new Error('Failed to publish series')
}
return {
id: published.id,
pubkey: params.authorPubkey,
title: params.title,
description: params.description,
preview: params.preview ?? params.description.substring(0, 200),
category,
...(params.coverUrl ? { coverUrl: params.coverUrl } : {}),
kindType: 'series',
}
}
function buildSeriesEvent(
params: {
title: string
description: string
preview?: string
coverUrl?: string
authorPubkey: string
},
category: NonNullable<ArticleDraft['category']>
) {
// Map category to new system
const newCategory = category === 'science-fiction' ? 'sciencefiction' : 'research'
return {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
content: params.preview ?? params.description.substring(0, 200),
tags: buildTags({
type: 'series',
category: newCategory,
id: '', // Will be set to event.id after publication
paywall: false,
title: params.title,
description: params.description,
preview: params.preview ?? params.description.substring(0, 200),
...(params.coverUrl ? { coverUrl: params.coverUrl } : {}),
}),
}
}
export async function publishReview(params: {
articleId: string
seriesId: string
category: ArticleDraft['category']
authorPubkey: string
reviewerPubkey: string
content: string
title?: string
authorPrivateKey?: string
}): Promise<Review> {
ensureKeys(params.reviewerPubkey, params.authorPrivateKey)
const category = params.category
requireCategory(category)
const event = buildReviewEvent(params, category)
const published = await nostrService.publishEvent(event)
if (!published) {
throw new Error('Failed to publish review')
}
return {
id: published.id,
articleId: params.articleId,
authorPubkey: params.authorPubkey,
reviewerPubkey: params.reviewerPubkey,
content: params.content,
createdAt: published.created_at,
...(params.title ? { title: params.title } : {}),
kindType: 'review',
}
}
function buildReviewEvent(
params: {
articleId: string
seriesId: string
authorPubkey: string
reviewerPubkey: string
content: string
title?: string
},
category: NonNullable<ArticleDraft['category']>
) {
// Map category to new system
const newCategory = category === 'science-fiction' ? 'sciencefiction' : 'research'
return {
kind: 1,
created_at: Math.floor(Date.now() / 1000),
content: params.content,
tags: buildTags({
type: 'quote',
category: newCategory,
id: '', // Will be set to event.id after publication
paywall: false,
articleId: params.articleId,
reviewerPubkey: params.reviewerPubkey,
...(params.title ? { title: params.title } : {}),
}),
}
}
function buildUpdateTags(draft: ArticleDraft, originalArticleId: string, newCategory: 'sciencefiction' | 'research') {
const updateTags = buildTags({
type: 'publication',
category: newCategory,
id: '', // Will be set to event.id after publication
paywall: true,
title: draft.title,
preview: draft.preview,
zapAmount: draft.zapAmount,
...(draft.seriesId ? { seriesId: draft.seriesId } : {}),
...(draft.bannerUrl ? { bannerUrl: draft.bannerUrl } : {}),
})
updateTags.push(['e', originalArticleId], ['replace', 'article-update'])
return updateTags
}
async function publishUpdate(
draft: ArticleDraft,
authorPubkey: string,
originalArticleId: string
): Promise<ArticleUpdateResult> {
const category = draft.category
requireCategory(category)
const presentationId = await ensurePresentation(authorPubkey)
const invoice = await createArticleInvoice(draft)
const newCategory = category === 'science-fiction' ? 'sciencefiction' : 'research'
const updateTags = buildUpdateTags(draft, originalArticleId, newCategory)
const publishedEvent = await publishPreviewWithInvoice(draft, invoice, presentationId, updateTags)
if (!publishedEvent) {
return updateFailure(originalArticleId, 'Failed to publish article update')
}
await storePrivateContent(publishedEvent.id, draft.content, authorPubkey, invoice)
return {
articleId: publishedEvent.id,
previewEventId: publishedEvent.id,
invoice,
success: true,
originalArticleId,
}
}
export async function publishArticleUpdate(
originalArticleId: string,
draft: ArticleDraft,
authorPubkey: string,
authorPrivateKey?: string
): Promise<ArticleUpdateResult> {
try {
ensureKeys(authorPubkey, authorPrivateKey)
return await publishUpdate(draft, authorPubkey, originalArticleId)
} catch (error) {
return updateFailure(originalArticleId, error instanceof Error ? error.message : 'Unknown error')
}
}
function updateFailure(originalArticleId: string, error?: string): ArticleUpdateResult {
return {
articleId: '',
previewEventId: '',
success: false,
originalArticleId,
...(error ? { error } : {}),
}
}
export async function deleteArticleEvent(articleId: string, authorPubkey: string, authorPrivateKey?: string): Promise<void> {
ensureKeys(authorPubkey, authorPrivateKey)
const deleteEvent = {
kind: 5,
created_at: Math.floor(Date.now() / 1000),
tags: [['e', articleId]] as string[][],
content: 'deleted',
} as const
const published = await nostrService.publishEvent(deleteEvent)
if (!published) {
throw new Error('Failed to publish delete event')
}
}
// Re-export for convenience to avoid circular imports in hooks
import { articlePublisher } from './articlePublisher'
export const getStoredContent = getStoredPrivateContent