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 type { Article } from '@/types/nostr' import { buildParsedArticleFromDraft as buildParsedArticleFromDraftCore } from './articleDraftToParsedArticle' import { getPublishRelays } from './relaySelection' 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 }> { return buildParsedArticleFromDraftCore({ draft, invoice, authorPubkey, }) } 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 published = await publishPreviewToRelays(params) if (!published) { return null } if (params.returnStatus) { return { event: published.event, relayStatuses: buildRelayStatuses({ relays: published.relays, published: published.published }), } } return published.event } async function publishPreviewToRelays(params: PublishPreviewParams): Promise<{ event: import('nostr-tools').Event relays: string[] published: false | string[] } | null> { const { article, hash, version, index } = await buildParsedArticleFromDraft(params.draft, params.invoice, params.authorPubkey) const previewEventTemplate = await createPreviewEvent({ draft: params.draft, invoice: params.invoice, authorPubkey: params.authorPubkey, authorPresentationId: params.presentationId, ...(params.extraTags ? { extraTags: params.extraTags } : {}), ...(params.encryptedContent ? { encryptedContent: params.encryptedContent } : {}), ...(params.encryptedKey ? { encryptedKey: params.encryptedKey } : {}), }) const privateKey = getPrivateKeyOrThrow() writeOrchestrator.setPrivateKey(privateKey) const secretKey = hexToBytes(privateKey) const event = finalizeEvent(previewEventTemplate, secretKey) const relays = await getPublishRelays() const result = await writeOrchestrator.writeAndPublish( { objectType: 'publication', hash, event, parsed: article, version, hidden: false, index, }, relays ) if (!result.success) { return null } return { event, relays, published: result.published } } function getPrivateKeyOrThrow(): string { const privateKey = nostrService.getPrivateKey() if (!privateKey) { throw new Error('Private key required for signing') } return privateKey } function buildRelayStatuses(params: { relays: string[]; published: false | string[] }): import('./publishResult').RelayPublishStatus[] { return params.relays.map((relayUrl) => { const isSuccess = Array.isArray(params.published) && params.published.includes(relayUrl) return { relayUrl, success: isSuccess, error: isSuccess ? undefined : 'Failed to publish', } }) } 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 encrypted = await encryptDraftForPublishing(params) const publishResult = await publishEncryptedPreview({ ...params, ...encrypted }) const normalized = normalizePublishResult(publishResult) if (!normalized) { return buildFailure('Failed to publish article') } await storePrivateContent({ articleId: normalized.event.id, content: params.draft.content, authorPubkey: params.authorPubkey, invoice: encrypted.invoice, decryptionKey: encrypted.key, decryptionIV: encrypted.iv, }) console.warn('Article published with encrypted content', { articleId: normalized.event.id, authorPubkey: params.authorPubkey, timestamp: new Date().toISOString(), relayStatuses: normalized.relayStatuses, }) return { articleId: normalized.event.id, previewEventId: normalized.event.id, invoice: encrypted.invoice, success: true, ...(normalized.relayStatuses ? { relayStatuses: normalized.relayStatuses } : {}), } } async function encryptDraftForPublishing(params: { draft: ArticleDraft authorPubkey: string authorPrivateKeyForEncryption: string }): Promise<{ encryptedContent: string encryptedKey: string key: string iv: string invoice: AlbyInvoice }> { const { encryptedContent, key, iv } = await encryptArticleContent(params.draft.content) const encryptedKey = await encryptDecryptionKey(key, iv, params.authorPrivateKeyForEncryption, params.authorPubkey) const invoice = await createArticleInvoice(params.draft) return { encryptedContent, encryptedKey, key, iv, invoice } } async function publishEncryptedPreview(params: { draft: ArticleDraft invoice: AlbyInvoice authorPubkey: string category: NonNullable presentationId: string encryptedContent: string encryptedKey: string }): Promise { const extraTags = buildArticleExtraTags(params.draft, params.category) return publishPreview({ draft: params.draft, invoice: params.invoice, authorPubkey: params.authorPubkey, presentationId: params.presentationId, extraTags, encryptedContent: params.encryptedContent, encryptedKey: params.encryptedKey, returnStatus: true, }) } function normalizePublishResult( value: import('nostr-tools').Event | null | PublishResult ): { event: import('nostr-tools').Event; relayStatuses: import('./publishResult').RelayPublishStatus[] | undefined } | null { if (!value) { return null } if ('event' in value && 'relayStatuses' in value) { if (!value.event) { return null } return { event: value.event, relayStatuses: value.relayStatuses } } if ('id' in value) { return { event: value, relayStatuses: undefined } } return null }