263 lines
8.3 KiB
TypeScript
263 lines
8.3 KiB
TypeScript
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, MIN_EVENT_DATE } from './platformConfig'
|
|
import { generateAuthorHashId } from './hashIdGenerator'
|
|
import { generateObjectUrl } from './urlGenerator'
|
|
import { getLatestVersion } from './versionManager'
|
|
import { objectCache } from './objectCache'
|
|
|
|
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/<hash>_<index>_<version>
|
|
const profileUrl = generateObjectUrl('author', hashId, index, version)
|
|
|
|
// Encode pubkey to npub (for metadata JSON)
|
|
const npub = nip19.npubEncode(authorPubkey)
|
|
|
|
// Build visible content message (without metadata JSON)
|
|
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 (stored in tag, not in content)
|
|
const profileJson = JSON.stringify({
|
|
authorName,
|
|
npub,
|
|
pubkey: authorPubkey,
|
|
presentation,
|
|
contentDescription,
|
|
mainnetAddress: draft.mainnetAddress,
|
|
pictureUrl: draft.pictureUrl,
|
|
category,
|
|
url: profileUrl,
|
|
version,
|
|
index,
|
|
})
|
|
|
|
// Build tags (profile JSON is in tag, not in content)
|
|
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 } : {}),
|
|
})
|
|
|
|
// Add JSON metadata as a tag (not in visible content)
|
|
tags.push(['json', profileJson])
|
|
|
|
return {
|
|
kind: 1 as const,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags,
|
|
content: visibleContent,
|
|
}
|
|
}
|
|
|
|
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 tag first (new format)
|
|
let profileData: {
|
|
authorName?: string
|
|
presentation?: string
|
|
contentDescription?: string
|
|
mainnetAddress?: string
|
|
pictureUrl?: string
|
|
} | null = null
|
|
|
|
if (tags.json) {
|
|
try {
|
|
profileData = JSON.parse(tags.json)
|
|
} catch (e) {
|
|
console.error('Error parsing JSON from tag:', e)
|
|
}
|
|
}
|
|
|
|
// Fallback to content format (for backward compatibility with old notes)
|
|
if (!profileData) {
|
|
// Try invisible format (with zero-width characters)
|
|
const invisibleJsonMatch = event.content.match(/[\u200B\u200C]\[Metadata JSON\][\u200B\u200C]\n[\u200B\u200C](.+)[\u200B\u200C]$/s)
|
|
if (invisibleJsonMatch && invisibleJsonMatch[1]) {
|
|
try {
|
|
// Remove zero-width characters from JSON
|
|
const cleanedJson = invisibleJsonMatch[1].replace(/[\u200B\u200C\u200D\u200E\u200F]/g, '').trim()
|
|
profileData = JSON.parse(cleanedJson)
|
|
} catch (e) {
|
|
console.error('Error parsing profile JSON from invisible content:', e)
|
|
}
|
|
}
|
|
|
|
// Fallback to visible format in content
|
|
if (!profileData) {
|
|
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 async function fetchAuthorPresentationFromPool(
|
|
pool: SimplePoolWithSub,
|
|
pubkey: string
|
|
): Promise<import('@/types/nostr').AuthorPresentationArticle | null> {
|
|
// Check cache first
|
|
const cached = await objectCache.getAuthorByPubkey(pubkey)
|
|
if (cached) {
|
|
return cached
|
|
}
|
|
|
|
const filters = [
|
|
{
|
|
...buildTagFilter({
|
|
type: 'author',
|
|
authorPubkey: pubkey,
|
|
service: PLATFORM_SERVICE,
|
|
}),
|
|
since: MIN_EVENT_DATE,
|
|
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 = async (value: import('@/types/nostr').AuthorPresentationArticle | null) => {
|
|
if (resolved) {
|
|
return
|
|
}
|
|
resolved = true
|
|
sub.unsub()
|
|
|
|
// Cache the result if found
|
|
if (value && events.length > 0) {
|
|
const event = events.find(e => e.id === value.id) || events[0]
|
|
if (event) {
|
|
const tags = extractTagsFromEvent(event)
|
|
if (tags.id) {
|
|
await objectCache.set('author', tags.id, event, value, tags.version, tags.hidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
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', async () => {
|
|
// Get the latest version from all collected events
|
|
const latestEvent = getLatestVersion(events)
|
|
if (latestEvent) {
|
|
const parsed = parsePresentationEvent(latestEvent)
|
|
if (parsed) {
|
|
await finalize(parsed)
|
|
return
|
|
}
|
|
}
|
|
await finalize(null)
|
|
})
|
|
setTimeout(async () => {
|
|
// Get the latest version from all collected events
|
|
const latestEvent = getLatestVersion(events)
|
|
if (latestEvent) {
|
|
const parsed = parsePresentationEvent(latestEvent)
|
|
if (parsed) {
|
|
await finalize(parsed)
|
|
return
|
|
}
|
|
}
|
|
await finalize(null)
|
|
}, 5000).unref?.()
|
|
})
|
|
}
|