story-research-zapwall/lib/articlePublisher.ts
2025-12-22 09:48:57 +01:00

183 lines
5.2 KiB
TypeScript

import { nostrService } from './nostr'
import { nip04 } from 'nostr-tools'
import type { Article } from '@/types/nostr'
import type { AlbyInvoice } from '@/types/alby'
import {
storePrivateContent,
getStoredPrivateContent,
getStoredInvoice,
removeStoredPrivateContent,
} from './articleStorage'
import { createArticleInvoice, createPreviewEvent } from './articleInvoice'
export interface ArticleDraft {
title: string
preview: string
content: string // Full content that will be sent as private message after payment
zapAmount: number
}
export interface PublishedArticle {
articleId: string
previewEventId: string
invoice?: AlbyInvoice // Invoice created by author (required if success)
success: boolean
error?: string
}
/**
* Service for publishing articles on Nostr
* Handles publishing preview (public note), creating invoice, and storing full content for later private message
*/
export class ArticlePublisher {
/**
* Publish an article preview as a public note (kind:1)
* Creates a Lightning invoice for the article
* The full content will be sent as encrypted private message after payment
*/
async publishArticle(
draft: ArticleDraft,
authorPubkey: string,
authorPrivateKey?: string
): Promise<PublishedArticle> {
try {
// Set author public key for publishing
nostrService.setPublicKey(authorPubkey)
// Set private key if provided (for direct signing)
// If not provided, will attempt to use remote signing
if (authorPrivateKey) {
nostrService.setPrivateKey(authorPrivateKey)
} else {
// Try to get private key from service (might be set by NostrConnect)
const existingPrivateKey = nostrService.getPrivateKey()
if (!existingPrivateKey) {
return {
articleId: '',
previewEventId: '',
success: false,
error: 'Private key required for signing. Please connect with a Nostr wallet that provides signing capabilities.',
}
}
}
// Create Lightning invoice via Alby/WebLN (author creates the invoice)
const invoice = await createArticleInvoice(draft)
// Create public note with preview and invoice
const previewEvent = createPreviewEvent(draft, invoice)
const publishedEvent = await nostrService.publishEvent(previewEvent)
if (!publishedEvent) {
return {
articleId: '',
previewEventId: '',
success: false,
error: 'Failed to publish article',
}
}
// Store the full content associated with this article ID
// Also store the invoice if created
await storePrivateContent(publishedEvent.id, draft.content, authorPubkey, invoice)
return {
articleId: publishedEvent.id,
previewEventId: publishedEvent.id,
invoice,
success: true,
}
} catch (error) {
console.error('Error publishing article:', error)
return {
articleId: '',
previewEventId: '',
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
}
}
}
/**
* Get stored private content for an article
*/
async getStoredPrivateContent(articleId: string): Promise<{
content: string
authorPubkey: string
invoice?: AlbyInvoice
} | null> {
return getStoredPrivateContent(articleId)
}
/**
* Get stored invoice for an article
*/
async getStoredInvoice(articleId: string): Promise<AlbyInvoice | null> {
return getStoredInvoice(articleId)
}
/**
* Send private content to a user after payment confirmation
*/
async sendPrivateContent(
articleId: string,
recipientPubkey: string,
authorPubkey: string,
authorPrivateKey: string
): Promise<boolean> {
try {
// Get stored private content
const stored = await getStoredPrivateContent(articleId)
if (!stored) {
console.error('Private content not found for article:', articleId)
return false
}
// Set author keys
nostrService.setPublicKey(authorPubkey)
nostrService.setPrivateKey(authorPrivateKey)
// Encrypt content using NIP-04
const encryptedContent = await nip04.encrypt(
authorPrivateKey,
recipientPubkey,
stored.content
)
// Create encrypted direct message (kind:4)
const privateMessageEvent = {
kind: 4,
created_at: Math.floor(Date.now() / 1000),
tags: [
['p', recipientPubkey],
['e', articleId], // Link to the article
],
content: encryptedContent,
}
const publishedEvent = await nostrService.publishEvent(privateMessageEvent)
if (publishedEvent) {
// Optionally remove stored content after successful send
// this.removeStoredPrivateContent(articleId)
return true
}
return false
} catch (error) {
console.error('Error sending private content:', error)
return false
}
}
/**
* Remove stored private content (after successful send or expiry)
*/
async removeStoredPrivateContent(articleId: string): Promise<void> {
await removeStoredPrivateContent(articleId)
}
}
export const articlePublisher = new ArticlePublisher()