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 { 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 { 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 { 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 { 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 ) { // 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 { 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 ) { // 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 { 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 { 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 { 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