import { nip04, type Event } from 'nostr-tools' import { nostrService } from './nostr' import type { AuthorPresentationDraft } from './articlePublisher' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' const RELAY_URL = process.env.NEXT_PUBLIC_NOSTR_RELAY_URL ?? 'wss://relay.damus.io' export function buildPresentationEvent(draft: AuthorPresentationDraft) { return { kind: 1 as const, created_at: Math.floor(Date.now() / 1000), tags: [ ['title', draft.title], ['preview', draft.preview], ['category', 'author-presentation'], ['presentation', 'true'], ['mainnet_address', draft.mainnetAddress], ['total_sponsoring', '0'], ['content-type', 'author-presentation'], ], content: draft.content, } } export function parsePresentationEvent(event: Event): import('@/types/nostr').AuthorPresentationArticle | null { const isPresentation = event.tags.some((tag) => tag[0] === 'presentation' && tag[1] === 'true') if (!isPresentation) { return null } const mainnetAddressTag = event.tags.find((tag) => tag[0] === 'mainnet_address') const sponsoringTag = event.tags.find((tag) => tag[0] === 'total_sponsoring') return { id: event.id, pubkey: event.pubkey, title: event.tags.find((tag) => tag[0] === 'title')?.[1] ?? 'Présentation', preview: event.tags.find((tag) => tag[0] === 'preview')?.[1] ?? event.content.substring(0, 200), content: event.content, createdAt: event.created_at, zapAmount: 0, paid: true, category: 'author-presentation', isPresentation: true, mainnetAddress: mainnetAddressTag?.[1] ?? '', totalSponsoring: sponsoringTag ? parseInt(sponsoringTag[1] ?? '0', 10) : 0, } } export function fetchAuthorPresentationFromPool( pool: SimplePoolWithSub, pubkey: string ): Promise { const filters = [ { kinds: [1], authors: [pubkey], '#category': ['author-presentation'], limit: 1, }, ] return new Promise((resolve) => { let resolved = false const sub = pool.sub([RELAY_URL], filters) const finalize = (value: import('@/types/nostr').AuthorPresentationArticle | null) => { if (resolved) { return } resolved = true sub.unsub() resolve(value) } sub.on('event', (event: Event) => { const parsed = parsePresentationEvent(event) if (parsed) { finalize(parsed) } }) sub.on('eose', () => finalize(null)) setTimeout(() => finalize(null), 5000) }) } export interface SendContentResult { success: boolean messageEventId?: string error?: string verified?: boolean } export async function sendEncryptedContent( articleId: string, recipientPubkey: string, storedContent: { content: string; authorPubkey: string }, authorPrivateKey: string ): Promise { try { nostrService.setPrivateKey(authorPrivateKey) nostrService.setPublicKey(storedContent.authorPubkey) const encryptedContent = await Promise.resolve(nip04.encrypt(authorPrivateKey, recipientPubkey, storedContent.content)) const privateMessageEvent = { kind: 4, created_at: Math.floor(Date.now() / 1000), tags: [ ['p', recipientPubkey], ['e', articleId], ], content: encryptedContent, } const publishedEvent = await nostrService.publishEvent(privateMessageEvent) if (!publishedEvent) { console.error('Failed to publish private message event', { articleId, recipientPubkey, authorPubkey: storedContent.authorPubkey, }) return { success: false, error: 'Failed to publish private message event', } } const messageEventId = publishedEvent.id console.log('Private message published', { messageEventId, articleId, recipientPubkey, authorPubkey: storedContent.authorPubkey, timestamp: new Date().toISOString(), }) const verified = await verifyPrivateMessagePublished(messageEventId, storedContent.authorPubkey, recipientPubkey, articleId) if (verified) { console.log('Private message verified on relay', { messageEventId, articleId, recipientPubkey, }) } else { console.warn('Private message published but not yet verified on relay', { messageEventId, articleId, recipientPubkey, }) } return { success: true, messageEventId, verified, } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' console.error('Error sending encrypted content', { articleId, recipientPubkey, authorPubkey: storedContent.authorPubkey, error: errorMessage, timestamp: new Date().toISOString(), }) return { success: false, error: errorMessage, } } } async function verifyPrivateMessagePublished( messageEventId: string, authorPubkey: string, recipientPubkey: string, articleId: string ): Promise { try { const pool = nostrService.getPool() if (!pool) { console.error('Pool not initialized for message verification', { messageEventId, articleId, recipientPubkey, }) return false } return new Promise((resolve) => { let resolved = false const filters = [ { kinds: [4], ids: [messageEventId], authors: [authorPubkey], '#p': [recipientPubkey], '#e': [articleId], limit: 1, }, ] const sub = (pool as import('@/types/nostr-tools-extended').SimplePoolWithSub).sub([RELAY_URL], filters) const finalize = (value: boolean) => { if (resolved) { return } resolved = true sub.unsub() resolve(value) } sub.on('event', (event) => { console.log('Private message verified on relay', { messageEventId: event.id, articleId, recipientPubkey, authorPubkey, timestamp: new Date().toISOString(), }) finalize(true) }) sub.on('eose', () => { console.warn('Private message not found on relay after EOSE', { messageEventId, articleId, recipientPubkey, timestamp: new Date().toISOString(), }) finalize(false) }) setTimeout(() => { if (!resolved) { console.warn('Timeout verifying private message on relay', { messageEventId, articleId, recipientPubkey, timestamp: new Date().toISOString(), }) finalize(false) } }, 5000) }) } catch (error) { console.error('Error verifying private message', { messageEventId, articleId, recipientPubkey, error: error instanceof Error ? error.message : 'Unknown error', timestamp: new Date().toISOString(), }) return false } }