import { nostrService } from './nostr' import type { ArticleDraft, PublishedArticle } from './articlePublisherTypes' import type { AlbyInvoice } from '@/types/alby' import { createArticleInvoice, createPreviewEvent } from './articleInvoice' import { encryptArticleContent, encryptDecryptionKey } from './articleEncryption' import { storePrivateContent } from './articleStorage' import type { PublishResult } from './publishResult' import { writeOrchestrator } from './writeOrchestrator' import { finalizeEvent } from 'nostr-tools' import { hexToBytes } from 'nostr-tools/utils' import { generatePublicationHashId } from './hashIdGenerator' import { buildObjectId } from './urlGenerator' import type { Article } from '@/types/nostr' export function buildFailure(error?: string): PublishedArticle { const base: PublishedArticle = { articleId: '', previewEventId: '', success: false, } return error ? { ...base, error } : base } async function buildParsedArticleFromDraft( draft: ArticleDraft, invoice: AlbyInvoice, authorPubkey: string ): Promise<{ article: Article; hash: string; version: number; index: number }> { const category = draft.category === 'science-fiction' ? 'sciencefiction' : draft.category === 'scientific-research' ? 'research' : 'sciencefiction' const hashId = await generatePublicationHashId({ pubkey: authorPubkey, title: draft.title, preview: draft.preview, category, seriesId: draft.seriesId ?? undefined, bannerUrl: draft.bannerUrl ?? undefined, zapAmount: draft.zapAmount, }) const hash = hashId const version = 0 const index = 0 const id = buildObjectId(hash, index, version) const article: Article = { id, hash, version, index, pubkey: authorPubkey, title: draft.title, preview: draft.preview, content: '', description: draft.preview, contentDescription: draft.preview, createdAt: Math.floor(Date.now() / 1000), zapAmount: draft.zapAmount, paid: false, thumbnailUrl: draft.bannerUrl ?? '', invoice: invoice.invoice, ...(invoice.paymentHash ? { paymentHash: invoice.paymentHash } : {}), ...(draft.category ? { category: draft.category } : {}), ...(draft.seriesId ? { seriesId: draft.seriesId } : {}), ...(draft.bannerUrl ? { bannerUrl: draft.bannerUrl } : {}), ...(draft.pages && draft.pages.length > 0 ? { pages: draft.pages } : {}), kindType: 'article', } return { article, hash, version, index } } export async function publishPreview( draft: ArticleDraft, invoice: AlbyInvoice, authorPubkey: string, presentationId: string, extraTags?: string[][], encryptedContent?: string, encryptedKey?: string, returnStatus?: boolean ): Promise { // Build parsed article object const { article, hash, version, index } = await buildParsedArticleFromDraft(draft, invoice, authorPubkey) // Build event template const previewEventTemplate = await createPreviewEvent(draft, invoice, authorPubkey, presentationId, extraTags, encryptedContent, encryptedKey) // Set private key in orchestrator const privateKey = nostrService.getPrivateKey() if (!privateKey) { throw new Error('Private key required for signing') } writeOrchestrator.setPrivateKey(privateKey) // Finalize event const secretKey = hexToBytes(privateKey) const event = finalizeEvent(previewEventTemplate, secretKey) // Get active relays const { relaySessionManager } = await import('./relaySessionManager') const activeRelays = await relaySessionManager.getActiveRelays() const { getPrimaryRelay } = await import('./config') const relays = activeRelays.length > 0 ? activeRelays : [await getPrimaryRelay()] // Publish via writeOrchestrator (parallel network + local write) const result = await writeOrchestrator.writeAndPublish( { objectType: 'publication', hash, event, parsed: article, version, hidden: false, index, }, relays ) if (!result.success) { return null } if (returnStatus) { // Return PublishResult format return { event, relayStatuses: relays.map((relayUrl, _idx) => { const isSuccess = typeof result.published === 'object' && result.published.includes(relayUrl) return { relayUrl, success: isSuccess, error: isSuccess ? undefined : 'Failed to publish', } }), } } return event } export function buildArticleExtraTags(draft: ArticleDraft, _category: NonNullable): string[][] { // Media tags are still supported in the new system const extraTags: string[][] = [] if (draft.media && draft.media.length > 0) { draft.media.forEach((m) => { extraTags.push(['media', m.url, m.type]) }) } return extraTags } export async function encryptAndPublish( draft: ArticleDraft, authorPubkey: string, authorPrivateKeyForEncryption: string, category: NonNullable, presentationId: string ): Promise { const { encryptedContent, key, iv } = await encryptArticleContent(draft.content) const encryptedKey = await encryptDecryptionKey(key, iv, authorPrivateKeyForEncryption, authorPubkey) const invoice = await createArticleInvoice(draft) const extraTags = buildArticleExtraTags(draft, category) const publishResult = await publishPreview(draft, invoice, authorPubkey, presentationId, extraTags, encryptedContent, encryptedKey, true) if (!publishResult) { return buildFailure('Failed to publish article') } // Handle both old format (Event | null) and new format (PublishResult) let event: import('nostr-tools').Event | null = null let relayStatuses: import('./publishResult').RelayPublishStatus[] | undefined if (publishResult && 'event' in publishResult && 'relayStatuses' in publishResult) { // New format with statuses event = publishResult.event relayStatuses = publishResult.relayStatuses } else if (publishResult && 'id' in publishResult) { // Old format (Event) event = publishResult } if (!event) { return buildFailure('Failed to publish article') } await storePrivateContent(event.id, draft.content, authorPubkey, invoice, key, iv) console.warn('Article published with encrypted content', { articleId: event.id, authorPubkey, timestamp: new Date().toISOString(), relayStatuses, }) return { articleId: event.id, previewEventId: event.id, invoice, success: true, relayStatuses, } }