import { nostrService } from './nostr' import { nip04 } from 'nostr-tools' import type { Article } from '@/types/nostr' import type { AlbyInvoice } from '@/types/alby' import { storePrivateContent, getStoredPrivateContent, getStoredInvoice, removeStoredPrivateContent, } from './articleStorage' import { createArticleInvoice, createPreviewEvent } from './articleInvoice' export interface ArticleDraft { title: string preview: string content: string // Full content that will be sent as private message after payment zapAmount: number } 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 { /** * 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 { try { // Set author public key for publishing nostrService.setPublicKey(authorPubkey) // Set private key if provided (for direct signing) // If not provided, will attempt to use remote signing if (authorPrivateKey) { nostrService.setPrivateKey(authorPrivateKey) } else { // Try to get private key from service (might be set by NostrConnect) const existingPrivateKey = nostrService.getPrivateKey() if (!existingPrivateKey) { return { articleId: '', previewEventId: '', success: false, error: 'Private key required for signing. Please connect with a Nostr wallet that provides signing capabilities.', } } } // Create Lightning invoice via Alby/WebLN (author creates the invoice) const invoice = await createArticleInvoice(draft) // Create public note with preview and invoice const previewEvent = createPreviewEvent(draft, invoice) const publishedEvent = await nostrService.publishEvent(previewEvent) if (!publishedEvent) { return { articleId: '', previewEventId: '', success: false, error: 'Failed to publish article', } } // Store the full content associated with this article ID // Also store the invoice if created 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 { articleId: '', previewEventId: '', success: false, error: error instanceof Error ? error.message : 'Unknown error', } } } /** * Get stored private content for an article */ async getStoredPrivateContent(articleId: string): Promise<{ content: string authorPubkey: string invoice?: AlbyInvoice } | null> { return getStoredPrivateContent(articleId) } /** * Get stored invoice for an article */ async getStoredInvoice(articleId: string): Promise { return getStoredInvoice(articleId) } /** * Send private content to a user after payment confirmation */ async sendPrivateContent( articleId: string, recipientPubkey: string, authorPubkey: string, authorPrivateKey: string ): Promise { try { // Get stored private content const stored = await getStoredPrivateContent(articleId) if (!stored) { console.error('Private content not found for article:', articleId) return false } // Set author keys nostrService.setPublicKey(authorPubkey) nostrService.setPrivateKey(authorPrivateKey) // Encrypt content using NIP-04 const encryptedContent = await nip04.encrypt( authorPrivateKey, recipientPubkey, stored.content ) // Create encrypted direct message (kind:4) const privateMessageEvent = { kind: 4, created_at: Math.floor(Date.now() / 1000), tags: [ ['p', recipientPubkey], ['e', articleId], // Link to the article ], content: encryptedContent, } const publishedEvent = await nostrService.publishEvent(privateMessageEvent) if (publishedEvent) { // Optionally remove stored content after successful send // this.removeStoredPrivateContent(articleId) return true } return false } 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 { await removeStoredPrivateContent(articleId) } } export const articlePublisher = new ArticlePublisher()