import { nip04, type Event } from 'nostr-tools' import { nostrService } from './nostr' import type { AuthorPresentationDraft } from './articlePublisher' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import { buildTags, extractTagsFromEvent, buildTagFilter } from './nostrTagSystem' const RELAY_URL = process.env.NEXT_PUBLIC_NOSTR_RELAY_URL ?? 'wss://relay.damus.io' export function buildPresentationEvent(draft: AuthorPresentationDraft, eventId: string, category: 'sciencefiction' | 'research' = 'sciencefiction') { return { kind: 1 as const, created_at: Math.floor(Date.now() / 1000), tags: buildTags({ type: 'author', category, id: eventId, paywall: false, title: draft.title, preview: draft.preview, mainnetAddress: draft.mainnetAddress, totalSponsoring: 0, ...(draft.pictureUrl ? { pictureUrl: draft.pictureUrl } : {}), }), content: draft.content, } } export function parsePresentationEvent(event: Event): import('@/types/nostr').AuthorPresentationArticle | null { const tags = extractTagsFromEvent(event) // Check if it's an author type (tag is 'author' in English) if (tags.type !== 'author') { return null } return { id: tags.id ?? event.id, pubkey: event.pubkey, title: (tags.title as string | undefined) ?? 'Présentation', preview: (tags.preview as string | undefined) ?? event.content.substring(0, 200), content: event.content, createdAt: event.created_at, zapAmount: 0, paid: true, category: 'author-presentation', isPresentation: true, mainnetAddress: (tags.mainnetAddress as string | undefined) ?? '', totalSponsoring: (tags.totalSponsoring as number | undefined) ?? 0, ...(tags.pictureUrl ? { bannerUrl: tags.pictureUrl as string } : {}), } } export function fetchAuthorPresentationFromPool( pool: SimplePoolWithSub, pubkey: string ): Promise { const filters = [ { ...buildTagFilter({ type: 'author', authorPubkey: pubkey, }), 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; decryptionKey?: string; decryptionIV?: string }, authorPrivateKey: string ): Promise { try { nostrService.setPrivateKey(authorPrivateKey) nostrService.setPublicKey(storedContent.authorPubkey) // Send the decryption key instead of the full content // The key is sent as JSON: { key: string, iv: string } const keyData = storedContent.decryptionKey && storedContent.decryptionIV ? JSON.stringify({ key: storedContent.decryptionKey, iv: storedContent.decryptionIV }) : storedContent.content // Fallback to old behavior if keys are not available const encryptedKey = await Promise.resolve(nip04.encrypt(authorPrivateKey, recipientPubkey, keyData)) const privateMessageEvent = { kind: 4, created_at: Math.floor(Date.now() / 1000), tags: [ ['p', recipientPubkey], ['e', articleId], ], content: encryptedKey, } 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 } }