183 lines
5.2 KiB
TypeScript
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()
|