story-research-zapwall/lib/articlePublisher.ts
Nicolas Cantu 3000872dbc refactoring
- **Motivations :** Assurer passage du lint strict et clarifier la logique paiements/publications.

- **Root causes :** Fonctions trop longues, promesses non gérées et typages WebLN/Nostr incomplets.

- **Correctifs :** Refactor PaymentModal (handlers void), extraction helpers articlePublisher, simplification polling sponsoring/zap, corrections curly et awaits.

- **Evolutions :** Nouveau module articlePublisherHelpers pour présentation/aiguillage contenu privé.

- **Page affectées :** components/PaymentModal.tsx, lib/articlePublisher.ts, lib/articlePublisherHelpers.ts, lib/paymentPolling.ts, lib/sponsoring.ts, lib/nostrZapVerification.ts et dépendances liées.
2025-12-22 17:56:00 +01:00

223 lines
6.7 KiB
TypeScript

import { nostrService } from './nostr'
import type { AlbyInvoice } from '@/types/alby'
import type { SimplePoolWithSub } from '@/types/nostr-tools-extended'
import {
storePrivateContent,
getStoredPrivateContent,
getStoredInvoice,
removeStoredPrivateContent,
} from './articleStorage'
import { createArticleInvoice, createPreviewEvent } from './articleInvoice'
import { buildPresentationEvent, fetchAuthorPresentationFromPool, sendEncryptedContent } from './articlePublisherHelpers'
export interface ArticleDraft {
title: string
preview: string
content: string // Full content that will be sent as private message after payment
zapAmount: number
category?: 'science-fiction' | 'scientific-research'
}
export interface AuthorPresentationDraft {
title: string
preview: string
content: string
presentation: string
contentDescription: string
mainnetAddress: string
}
export interface PublishedArticle {
articleId: string
previewEventId: string
invoice?: AlbyInvoice // Invoice created by author (required if success)
success: boolean
error?: string
}
/**
* Service for publishing articles on Nostr
* Handles publishing preview (public note), creating invoice, and storing full content for later private message
*/
export class ArticlePublisher {
private buildFailure(error?: string): PublishedArticle {
return {
articleId: '',
previewEventId: '',
success: false,
error,
}
}
private prepareAuthorKeys(authorPubkey: string, authorPrivateKey?: string): { success: boolean; error?: string } {
nostrService.setPublicKey(authorPubkey)
if (authorPrivateKey) {
nostrService.setPrivateKey(authorPrivateKey)
return { success: true }
}
const existingPrivateKey = nostrService.getPrivateKey()
if (!existingPrivateKey) {
return {
success: false,
error:
'Private key required for signing. Please connect with a Nostr wallet that provides signing capabilities.',
}
}
return { success: true }
}
private isValidCategory(category?: ArticleDraft['category']): category is ArticleDraft['category'] {
return category === 'science-fiction' || category === 'scientific-research'
}
private async publishPreview(
draft: ArticleDraft,
invoice: AlbyInvoice,
presentationId: string
): Promise<import('nostr-tools').Event | null> {
const previewEvent = createPreviewEvent(draft, invoice, presentationId)
const publishedEvent = await nostrService.publishEvent(previewEvent)
return publishedEvent ?? null
}
/**
* Publish an article preview as a public note (kind:1)
* Creates a Lightning invoice for the article
* The full content will be sent as encrypted private message after payment
*/
async publishArticle(
draft: ArticleDraft,
authorPubkey: string,
authorPrivateKey?: string
): Promise<PublishedArticle> {
try {
const keySetup = this.prepareAuthorKeys(authorPubkey, authorPrivateKey)
if (!keySetup.success) {
return this.buildFailure(keySetup.error)
}
const presentation = await this.getAuthorPresentation(authorPubkey)
if (!presentation) {
return this.buildFailure('Vous devez créer un article de présentation avant de publier des articles.')
}
if (!this.isValidCategory(draft.category)) {
return this.buildFailure('Vous devez sélectionner une catégorie (science-fiction ou recherche scientifique).')
}
const invoice = await createArticleInvoice(draft)
const publishedEvent = await this.publishPreview(draft, invoice, presentation.id)
if (!publishedEvent) {
return this.buildFailure('Failed to publish article')
}
await storePrivateContent(publishedEvent.id, draft.content, authorPubkey, invoice)
return { articleId: publishedEvent.id, previewEventId: publishedEvent.id, invoice, success: true }
} catch (error) {
console.error('Error publishing article:', error)
return this.buildFailure(error instanceof Error ? error.message : 'Unknown error')
}
}
/**
* Get stored private content for an article
*/
getStoredPrivateContent(articleId: string): Promise<{
content: string
authorPubkey: string
invoice?: AlbyInvoice
} | null> {
return getStoredPrivateContent(articleId)
}
/**
* Get stored invoice for an article
*/
getStoredInvoice(articleId: string): Promise<AlbyInvoice | null> {
return getStoredInvoice(articleId)
}
/**
* Send private content to a user after payment confirmation
*/
async sendPrivateContent(
articleId: string,
recipientPubkey: string,
authorPubkey: string,
authorPrivateKey: string
): Promise<boolean> {
try {
const stored = await getStoredPrivateContent(articleId)
if (!stored) {
console.error('Private content not found for article:', articleId)
return false
}
const sent = await sendEncryptedContent(articleId, recipientPubkey, stored, authorPrivateKey)
return sent
} catch (error) {
console.error('Error sending private content:', error)
return false
}
}
/**
* Remove stored private content (after successful send or expiry)
*/
async removeStoredPrivateContent(articleId: string): Promise<void> {
await removeStoredPrivateContent(articleId)
}
/**
* Publish an author presentation article (obligatory for all authors)
* This article is free and contains the author's presentation, content description, and mainnet address
*/
async publishPresentationArticle(
draft: AuthorPresentationDraft,
authorPubkey: string,
authorPrivateKey: string
): Promise<PublishedArticle> {
try {
nostrService.setPublicKey(authorPubkey)
nostrService.setPrivateKey(authorPrivateKey)
const publishedEvent = await nostrService.publishEvent(buildPresentationEvent(draft))
if (!publishedEvent) {
return this.buildFailure('Failed to publish presentation article')
}
return {
articleId: publishedEvent.id,
previewEventId: publishedEvent.id,
success: true,
}
} catch (error) {
console.error('Error publishing presentation article:', error)
return this.buildFailure(error instanceof Error ? error.message : 'Unknown error')
}
}
/**
* Get author presentation article by pubkey
*/
getAuthorPresentation(pubkey: string): Promise<import('@/types/nostr').AuthorPresentationArticle | null> {
try {
const pool = nostrService.getPool()
if (!pool) {
return null
}
return fetchAuthorPresentationFromPool(pool as SimplePoolWithSub, pubkey)
} catch (error) {
console.error('Error getting author presentation:', error)
return null
}
}
}
export const articlePublisher = new ArticlePublisher()