230 lines
7.1 KiB
TypeScript
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
|
|
}
|