story-research-zapwall/lib/articlePublisherPublish.ts
2026-01-10 10:50:47 +01:00

230 lines
7.1 KiB
TypeScript

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<import('nostr-tools').Event | null | PublishResult> {
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<ArticleDraft['category']>): 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<ArticleDraft['category']>
presentationId: string
}
): Promise<PublishedArticle> {
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<ArticleDraft['category']>
presentationId: string
encryptedContent: string
encryptedKey: string
}): Promise<import('nostr-tools').Event | null | PublishResult> {
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
}