import { type Event } from 'nostr-tools' import { nip19 } from 'nostr-tools' import type { AuthorPresentationDraft } from './articlePublisher' import type { SimplePoolWithSub } from '@/types/nostr-tools-extended' import { buildTags, extractTagsFromEvent, buildTagFilter } from './nostrTagSystem' import { getPrimaryRelaySync } from './config' import { PLATFORM_SERVICE } from './platformConfig' import { generateAuthorHashId } from './hashIdGenerator' import { generateObjectUrl } from './urlGenerator' import { getLatestVersion } from './versionManager' export async function buildPresentationEvent( draft: AuthorPresentationDraft, authorPubkey: string, authorName: string, category: 'sciencefiction' | 'research' = 'sciencefiction', version: number = 0, index: number = 0 ) { // Extract presentation and contentDescription from draft.content // Format: "${presentation}\n\n---\n\nDescription du contenu :\n${contentDescription}" const separator = '\n\n---\n\nDescription du contenu :\n' const separatorIndex = draft.content.indexOf(separator) const presentation = separatorIndex !== -1 ? draft.content.substring(0, separatorIndex) : draft.presentation const contentDescription = separatorIndex !== -1 ? draft.content.substring(separatorIndex + separator.length) : draft.contentDescription // Generate hash ID from author data first (needed for URL) const hashId = await generateAuthorHashId({ pubkey: authorPubkey, authorName, presentation, contentDescription, mainnetAddress: draft.mainnetAddress ?? undefined, pictureUrl: draft.pictureUrl ?? undefined, category, }) // Build URL: https://zapwall.fr/author/__ const profileUrl = generateObjectUrl('author', hashId, index, version) // Encode pubkey to npub (for metadata JSON) const npub = nip19.npubEncode(authorPubkey) // Build visible content message const visibleContent = [ 'Nouveau profil publié sur zapwall.fr', profileUrl, ...(draft.pictureUrl ? [draft.pictureUrl] : []), `Présentation personnelle : ${presentation}`, `Description de votre contenu : ${contentDescription}`, `Adresse Bitcoin mainnet (pour le sponsoring) : ${draft.mainnetAddress}`, ].join('\n') // Build profile JSON for metadata (non-visible) const profileJson = JSON.stringify({ authorName, npub, pubkey: authorPubkey, presentation, contentDescription, mainnetAddress: draft.mainnetAddress, pictureUrl: draft.pictureUrl, category, url: profileUrl, version, index, }, null, 2) // Combine visible content and JSON metadata (JSON in hidden section) const fullContent = `${visibleContent}\n\n---\n\n[Metadata JSON]\n${profileJson}` // Build tags (profile JSON is in content, not in tags) const tags = buildTags({ type: 'author', category, id: hashId, service: PLATFORM_SERVICE, version, hidden: false, paywall: false, title: draft.title, preview: draft.preview, mainnetAddress: draft.mainnetAddress, totalSponsoring: 0, ...(draft.pictureUrl ? { pictureUrl: draft.pictureUrl } : {}), }) return { kind: 1 as const, created_at: Math.floor(Date.now() / 1000), tags, content: fullContent, } } 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 } // Try to extract profile JSON from content [Metadata JSON] section let profileData: { presentation?: string contentDescription?: string mainnetAddress?: string pictureUrl?: string } | null = null const jsonMatch = event.content.match(/\[Metadata JSON\]\n(.+)$/s) if (jsonMatch && jsonMatch[1]) { try { profileData = JSON.parse(jsonMatch[1].trim()) } catch (e) { console.error('Error parsing profile JSON from content:', e) } } // Map tag category to article category const articleCategory = tags.category === 'sciencefiction' ? 'science-fiction' : tags.category === 'research' ? 'scientific-research' : undefined const result: import('@/types/nostr').AuthorPresentationArticle = { id: tags.id ?? event.id, pubkey: event.pubkey, title: tags.title ?? 'Présentation', preview: tags.preview ?? event.content.substring(0, 200), content: event.content, createdAt: event.created_at, zapAmount: 0, paid: true, category: 'author-presentation', isPresentation: true, mainnetAddress: profileData?.mainnetAddress ?? tags.mainnetAddress ?? '', totalSponsoring: tags.totalSponsoring ?? 0, originalCategory: articleCategory ?? 'science-fiction', // Store original category for filtering } // Add bannerUrl if available if (profileData?.pictureUrl !== undefined && profileData?.pictureUrl !== null) { result.bannerUrl = profileData.pictureUrl } else if (tags.pictureUrl !== undefined && tags.pictureUrl !== null && typeof tags.pictureUrl === 'string') { result.bannerUrl = tags.pictureUrl } return result } export function fetchAuthorPresentationFromPool( pool: SimplePoolWithSub, pubkey: string ): Promise { const filters = [ { ...buildTagFilter({ type: 'author', authorPubkey: pubkey, service: PLATFORM_SERVICE, }), limit: 100, // Get all versions to find the latest }, ] return new Promise((resolve) => { let resolved = false const relayUrl = getPrimaryRelaySync() const { createSubscription } = require('@/types/nostr-tools-extended') const sub = createSubscription(pool, [relayUrl], filters) const events: Event[] = [] const finalize = (value: import('@/types/nostr').AuthorPresentationArticle | null) => { if (resolved) { return } resolved = true sub.unsub() resolve(value) } sub.on('event', (event: Event) => { // Collect all events first const tags = extractTagsFromEvent(event) if (tags.type === 'author' && !tags.hidden) { events.push(event) } }) sub.on('eose', () => { // Get the latest version from all collected events const latestEvent = getLatestVersion(events) if (latestEvent) { const parsed = parsePresentationEvent(latestEvent) if (parsed) { finalize(parsed) return } } finalize(null) }) setTimeout(() => { // Get the latest version from all collected events const latestEvent = getLatestVersion(events) if (latestEvent) { const parsed = parsePresentationEvent(latestEvent) if (parsed) { finalize(parsed) return } } finalize(null) }, 5000).unref?.() }) }