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 }> { let category: string if (draft.category === 'science-fiction') { category = 'sciencefiction' } else if (draft.category === 'scientific-research') { category = 'research' } else { category = '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 } } interface PublishPreviewParams { draft: ArticleDraft invoice: AlbyInvoice authorPubkey: string presentationId: string extraTags?: string[][] encryptedContent?: string encryptedKey?: string returnStatus?: boolean } export async function publishPreview( params: PublishPreviewParams ): Promise { const { draft, invoice, authorPubkey, presentationId, extraTags, encryptedContent, encryptedKey, returnStatus } = params // Build parsed article object const { article, hash, version, index } = await buildParsedArticleFromDraft(draft, invoice, authorPubkey) // Build event template const previewEventTemplate = await createPreviewEvent({ draft, invoice, authorPubkey, authorPresentationId: presentationId, ...(extraTags ? { extraTags } : {}), ...(encryptedContent ? { encryptedContent } : {}), ...(encryptedKey ? { 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( params: { draft: ArticleDraft authorPubkey: string authorPrivateKeyForEncryption: string category: NonNullable presentationId: string } ): Promise { const { draft, authorPubkey, authorPrivateKeyForEncryption, category, presentationId } = params 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, returnStatus: 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, relayStatuses } = publishResult) } else if (publishResult && 'id' in publishResult) { // Old format (Event) event = publishResult } if (!event) { return buildFailure('Failed to publish article') } await storePrivateContent({ articleId: event.id, content: draft.content, authorPubkey, invoice, decryptionKey: key, decryptionIV: 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, } }