227 lines
7.0 KiB
TypeScript
227 lines
7.0 KiB
TypeScript
import { nostrService } from './nostr'
|
|
import { createArticleInvoice, createPreviewEvent } from './articleInvoice'
|
|
import { storePrivateContent, getStoredPrivateContent } from './articleStorage'
|
|
import { buildReviewTags, buildSeriesTags } from './nostrTags'
|
|
import type { ArticleDraft, PublishedArticle } from './articlePublisher'
|
|
import type { AlbyInvoice } from '@/types/alby'
|
|
import type { Review, Series } from '@/types/nostr'
|
|
|
|
export interface ArticleUpdateResult extends PublishedArticle {
|
|
originalArticleId: string
|
|
}
|
|
|
|
const SITE_TAG = process.env.NEXT_PUBLIC_SITE_TAG ?? 'zapwall4science'
|
|
|
|
function ensureKeys(authorPubkey: string, authorPrivateKey?: string): void {
|
|
nostrService.setPublicKey(authorPubkey)
|
|
if (authorPrivateKey) {
|
|
nostrService.setPrivateKey(authorPrivateKey)
|
|
} else if (!nostrService.getPrivateKey()) {
|
|
throw new Error('Private key required for signing. Connect a wallet that can sign.')
|
|
}
|
|
}
|
|
|
|
function requireCategory(category?: ArticleDraft['category']): asserts category is NonNullable<ArticleDraft['category']> {
|
|
if (category !== 'science-fiction' && category !== 'scientific-research') {
|
|
throw new Error('Vous devez sélectionner une catégorie (science-fiction ou recherche scientifique).')
|
|
}
|
|
}
|
|
|
|
async function ensurePresentation(authorPubkey: string): Promise<string> {
|
|
const presentation = await articlePublisher.getAuthorPresentation(authorPubkey)
|
|
if (!presentation) {
|
|
throw new Error('Vous devez créer un article de présentation avant de publier des articles.')
|
|
}
|
|
return presentation.id
|
|
}
|
|
|
|
async function publishPreviewWithInvoice(
|
|
draft: ArticleDraft,
|
|
invoice: AlbyInvoice,
|
|
presentationId: string,
|
|
extraTags?: string[][]
|
|
): Promise<import('nostr-tools').Event | null> {
|
|
const previewEvent = createPreviewEvent(draft, invoice, presentationId, extraTags)
|
|
const publishedEvent = await nostrService.publishEvent(previewEvent)
|
|
return publishedEvent ?? null
|
|
}
|
|
|
|
export async function publishSeries(params: {
|
|
title: string
|
|
description: string
|
|
preview?: string
|
|
coverUrl?: string
|
|
category: ArticleDraft['category']
|
|
authorPubkey: string
|
|
authorPrivateKey?: string
|
|
}): Promise<Series> {
|
|
ensureKeys(params.authorPubkey, params.authorPrivateKey)
|
|
const category = params.category
|
|
requireCategory(category)
|
|
const event = buildSeriesEvent(params, category)
|
|
const published = await nostrService.publishEvent(event)
|
|
if (!published) {
|
|
throw new Error('Failed to publish series')
|
|
}
|
|
return {
|
|
id: published.id,
|
|
pubkey: params.authorPubkey,
|
|
title: params.title,
|
|
description: params.description,
|
|
preview: params.preview ?? params.description.substring(0, 200),
|
|
category,
|
|
...(params.coverUrl ? { coverUrl: params.coverUrl } : {}),
|
|
kindType: 'series',
|
|
}
|
|
}
|
|
|
|
function buildSeriesEvent(
|
|
params: {
|
|
title: string
|
|
description: string
|
|
preview?: string
|
|
coverUrl?: string
|
|
authorPubkey: string
|
|
},
|
|
category: NonNullable<ArticleDraft['category']>
|
|
) {
|
|
return {
|
|
kind: 1,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
content: params.preview ?? params.description.substring(0, 200),
|
|
tags: buildSeriesTags({
|
|
site: SITE_TAG,
|
|
category,
|
|
author: params.authorPubkey,
|
|
seriesId: 'pending',
|
|
title: params.title,
|
|
description: params.description,
|
|
...(params.preview ? { preview: params.preview } : { preview: params.description.substring(0, 200) }),
|
|
...(params.coverUrl ? { coverUrl: params.coverUrl } : {}),
|
|
kindType: 'series',
|
|
}),
|
|
}
|
|
}
|
|
|
|
export async function publishReview(params: {
|
|
articleId: string
|
|
seriesId: string
|
|
category: ArticleDraft['category']
|
|
authorPubkey: string
|
|
reviewerPubkey: string
|
|
content: string
|
|
title?: string
|
|
authorPrivateKey?: string
|
|
}): Promise<Review> {
|
|
ensureKeys(params.reviewerPubkey, params.authorPrivateKey)
|
|
const category = params.category
|
|
requireCategory(category)
|
|
const event = buildReviewEvent(params, category)
|
|
const published = await nostrService.publishEvent(event)
|
|
if (!published) {
|
|
throw new Error('Failed to publish review')
|
|
}
|
|
return {
|
|
id: published.id,
|
|
articleId: params.articleId,
|
|
authorPubkey: params.authorPubkey,
|
|
reviewerPubkey: params.reviewerPubkey,
|
|
content: params.content,
|
|
createdAt: published.created_at,
|
|
...(params.title ? { title: params.title } : {}),
|
|
kindType: 'review',
|
|
}
|
|
}
|
|
|
|
function buildReviewEvent(
|
|
params: {
|
|
articleId: string
|
|
seriesId: string
|
|
authorPubkey: string
|
|
reviewerPubkey: string
|
|
content: string
|
|
title?: string
|
|
},
|
|
category: NonNullable<ArticleDraft['category']>
|
|
) {
|
|
return {
|
|
kind: 1,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
content: params.content,
|
|
tags: buildReviewTags({
|
|
site: SITE_TAG,
|
|
category,
|
|
author: params.authorPubkey,
|
|
seriesId: params.seriesId,
|
|
articleId: params.articleId,
|
|
reviewer: params.reviewerPubkey,
|
|
...(params.title ? { title: params.title } : {}),
|
|
kindType: 'review',
|
|
}),
|
|
}
|
|
}
|
|
|
|
export async function publishArticleUpdate(
|
|
originalArticleId: string,
|
|
draft: ArticleDraft,
|
|
authorPubkey: string,
|
|
authorPrivateKey?: string
|
|
): Promise<ArticleUpdateResult> {
|
|
try {
|
|
ensureKeys(authorPubkey, authorPrivateKey)
|
|
const category = draft.category
|
|
requireCategory(category)
|
|
const presentationId = await ensurePresentation(authorPubkey)
|
|
const invoice = await createArticleInvoice(draft)
|
|
const publishedEvent = await publishPreviewWithInvoice(draft, invoice, presentationId, [
|
|
['e', originalArticleId],
|
|
['replace', 'article-update'],
|
|
['kind_type', 'article'],
|
|
['site', SITE_TAG],
|
|
['category', category],
|
|
...(draft.seriesId ? [['series', draft.seriesId]] : []),
|
|
])
|
|
if (!publishedEvent) {
|
|
return updateFailure(originalArticleId, 'Failed to publish article update')
|
|
}
|
|
await storePrivateContent(publishedEvent.id, draft.content, authorPubkey, invoice)
|
|
return {
|
|
articleId: publishedEvent.id,
|
|
previewEventId: publishedEvent.id,
|
|
invoice,
|
|
success: true,
|
|
originalArticleId,
|
|
}
|
|
} catch (error) {
|
|
return updateFailure(originalArticleId, error instanceof Error ? error.message : 'Unknown error')
|
|
}
|
|
}
|
|
|
|
function updateFailure(originalArticleId: string, error?: string): ArticleUpdateResult {
|
|
return {
|
|
articleId: '',
|
|
previewEventId: '',
|
|
success: false,
|
|
originalArticleId,
|
|
...(error ? { error } : {}),
|
|
}
|
|
}
|
|
|
|
export async function deleteArticleEvent(articleId: string, authorPubkey: string, authorPrivateKey?: string): Promise<void> {
|
|
ensureKeys(authorPubkey, authorPrivateKey)
|
|
const deleteEvent = {
|
|
kind: 5,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [['e', articleId]] as string[][],
|
|
content: 'deleted',
|
|
} as const
|
|
const published = await nostrService.publishEvent(deleteEvent)
|
|
if (!published) {
|
|
throw new Error('Failed to publish delete event')
|
|
}
|
|
}
|
|
|
|
// Re-export for convenience to avoid circular imports in hooks
|
|
import { articlePublisher } from './articlePublisher'
|
|
export const getStoredContent = getStoredPrivateContent
|