- Intégration mempool.space pour vérification transactions Bitcoin : - Service MempoolSpaceService avec API mempool.space - Vérification sorties et montants pour sponsoring - Vérification confirmations - Attente confirmation avec polling - Récupération adresses Lightning depuis profils Nostr : - Service LightningAddressService - Support lud16 et lud06 (NIP-19) - Cache avec TTL 1 heure - Intégré dans paymentPolling et reviewReward - Mise à jour événements Nostr pour avis rémunérés : - Publication événement avec tags rewarded et reward_amount - Parsing tags dans parseReviewFromEvent - Vérification doublons - Tracking sponsoring sur Nostr : - Service SponsoringTrackingService - Événements avec commissions et confirmations - Intégration vérification mempool.space Toutes les fonctionnalités de split sont maintenant opérationnelles. Seuls les transferts Lightning réels nécessitent un nœud Lightning.
151 lines
4.9 KiB
TypeScript
151 lines
4.9 KiB
TypeScript
import type { Event } from 'nostr-tools'
|
|
import type { Article, KindType, MediaRef, Review, Series } from '@/types/nostr'
|
|
|
|
/**
|
|
* Parse article metadata from Nostr event
|
|
*/
|
|
export function parseArticleFromEvent(event: Event): Article | null {
|
|
try {
|
|
const tags = extractTags(event)
|
|
if (tags.kindType && tags.kindType !== 'article') {
|
|
return null
|
|
}
|
|
const { previewContent } = getPreviewContent(event.content, tags.preview)
|
|
return buildArticle(event, tags, previewContent)
|
|
} catch (e) {
|
|
console.error('Error parsing article:', e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function parseSeriesFromEvent(event: Event): Series | null {
|
|
try {
|
|
const tags = extractTags(event)
|
|
if (tags.kindType && tags.kindType !== 'series') {
|
|
return null
|
|
}
|
|
if (!tags.title || !tags.description) {
|
|
return null
|
|
}
|
|
const series: Series = {
|
|
id: event.id,
|
|
pubkey: event.pubkey,
|
|
title: tags.title,
|
|
description: tags.description,
|
|
preview: tags.preview ?? event.content.substring(0, 200),
|
|
...(tags.category ? { category: tags.category } : { category: 'science-fiction' }),
|
|
...(tags.coverUrl ? { coverUrl: tags.coverUrl } : {}),
|
|
}
|
|
if (tags.kindType) {
|
|
series.kindType = tags.kindType
|
|
}
|
|
return series
|
|
} catch (e) {
|
|
console.error('Error parsing series:', e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function parseReviewFromEvent(event: Event): Review | null {
|
|
try {
|
|
const tags = extractTags(event)
|
|
if (tags.kindType && tags.kindType !== 'review') {
|
|
return null
|
|
}
|
|
const articleId = tags.articleId
|
|
const reviewer = tags.reviewerPubkey
|
|
if (!articleId || !reviewer) {
|
|
return null
|
|
}
|
|
const rewardedTag = event.tags.find((tag) => tag[0] === 'rewarded' && tag[1] === 'true')
|
|
const rewardAmountTag = event.tags.find((tag) => tag[0] === 'reward_amount')
|
|
|
|
const review: Review = {
|
|
id: event.id,
|
|
articleId,
|
|
authorPubkey: tags.author ?? event.pubkey,
|
|
reviewerPubkey: reviewer,
|
|
content: event.content,
|
|
createdAt: event.created_at,
|
|
...(tags.title ? { title: tags.title } : {}),
|
|
...(rewardedTag ? { rewarded: true } : {}),
|
|
...(rewardAmountTag ? { rewardAmount: parseInt(rewardAmountTag[1] ?? '0', 10) } : {}),
|
|
}
|
|
if (tags.kindType) {
|
|
review.kindType = tags.kindType
|
|
}
|
|
return review
|
|
} catch (e) {
|
|
console.error('Error parsing review:', e)
|
|
return null
|
|
}
|
|
}
|
|
|
|
function extractTags(event: Event) {
|
|
const findTag = (key: string) => event.tags.find((tag) => tag[0] === key)?.[1]
|
|
const mediaTags = event.tags.filter((tag) => tag[0] === 'media')
|
|
const media: MediaRef[] =
|
|
mediaTags
|
|
.map((tag) => {
|
|
const url = tag[1]
|
|
const type = tag[2] === 'video' ? 'video' : 'image'
|
|
if (!url) {
|
|
return null
|
|
}
|
|
return { url, type }
|
|
})
|
|
.filter(Boolean) as MediaRef[]
|
|
|
|
return {
|
|
title: findTag('title') ?? 'Untitled',
|
|
preview: findTag('preview'),
|
|
description: findTag('description'),
|
|
zapAmount: parseInt(findTag('zap') ?? '800', 10),
|
|
invoice: findTag('invoice'),
|
|
paymentHash: findTag('payment_hash'),
|
|
category: findTag('category') as import('@/types/nostr').ArticleCategory | undefined,
|
|
isPresentation: findTag('presentation') === 'true',
|
|
mainnetAddress: findTag('mainnet_address'),
|
|
totalSponsoring: parseInt(findTag('total_sponsoring') ?? '0', 10),
|
|
authorPresentationId: findTag('author_presentation_id'),
|
|
seriesId: findTag('series'),
|
|
bannerUrl: findTag('banner'),
|
|
coverUrl: findTag('cover'),
|
|
media,
|
|
kindType: findTag('kind_type') as KindType | undefined,
|
|
articleId: findTag('article'),
|
|
reviewerPubkey: findTag('reviewer'),
|
|
author: findTag('author'),
|
|
}
|
|
}
|
|
|
|
function getPreviewContent(content: string, previewTag?: string) {
|
|
const lines = content.split('\n')
|
|
const previewContent = previewTag ?? lines[0] ?? content.substring(0, 200)
|
|
return { previewContent }
|
|
}
|
|
|
|
function buildArticle(event: Event, tags: ReturnType<typeof extractTags>, preview: string): Article {
|
|
return {
|
|
id: event.id,
|
|
pubkey: event.pubkey,
|
|
title: tags.title,
|
|
preview,
|
|
content: '',
|
|
createdAt: event.created_at,
|
|
zapAmount: tags.zapAmount,
|
|
paid: false,
|
|
...(tags.invoice ? { invoice: tags.invoice } : {}),
|
|
...(tags.paymentHash ? { paymentHash: tags.paymentHash } : {}),
|
|
...(tags.category ? { category: tags.category } : {}),
|
|
...(tags.isPresentation ? { isPresentation: tags.isPresentation } : {}),
|
|
...(tags.mainnetAddress ? { mainnetAddress: tags.mainnetAddress } : {}),
|
|
...(tags.totalSponsoring ? { totalSponsoring: tags.totalSponsoring } : {}),
|
|
...(tags.authorPresentationId ? { authorPresentationId: tags.authorPresentationId } : {}),
|
|
...(tags.seriesId ? { seriesId: tags.seriesId } : {}),
|
|
...(tags.bannerUrl ? { bannerUrl: tags.bannerUrl } : {}),
|
|
...(tags.media.length ? { media: tags.media } : {}),
|
|
...(tags.kindType ? { kindType: tags.kindType } : {}),
|
|
}
|
|
}
|