mirror of
https://github.com/dergigi/boris.git
synced 2026-02-16 04:24:25 +01:00
162 lines
7.4 KiB
TypeScript
162 lines
7.4 KiB
TypeScript
import { RelayPool, completeOnEose } from 'applesauce-relay'
|
|
import { lastValueFrom, merge, Observable, takeUntil, timer, toArray } from 'rxjs'
|
|
import {
|
|
AccountWithExtension,
|
|
NostrEvent,
|
|
dedupeNip51Events,
|
|
hydrateItems,
|
|
isAccountWithExtension,
|
|
isHexId,
|
|
hasNip04Decrypt,
|
|
hasNip44Decrypt,
|
|
dedupeBookmarksById,
|
|
extractUrlsFromContent
|
|
} from './bookmarkHelpers'
|
|
import { Bookmark } from '../types/bookmarks'
|
|
import { collectBookmarksFromEvents } from './bookmarkProcessing.ts'
|
|
import { UserSettings } from './settingsService'
|
|
import { rebroadcastEvents } from './rebroadcastService'
|
|
import { prioritizeLocalRelays, partitionRelays } from '../utils/helpers'
|
|
|
|
|
|
|
|
export const fetchBookmarks = async (
|
|
relayPool: RelayPool,
|
|
activeAccount: unknown, // Full account object with extension capabilities
|
|
setBookmarks: (bookmarks: Bookmark[]) => void,
|
|
settings?: UserSettings
|
|
) => {
|
|
try {
|
|
|
|
if (!isAccountWithExtension(activeAccount)) {
|
|
throw new Error('Invalid account object provided')
|
|
}
|
|
// Get relay URLs from the pool
|
|
const relayUrls = prioritizeLocalRelays(Array.from(relayPool.relays.values()).map(relay => relay.url))
|
|
const { local: localRelays, remote: remoteRelays } = partitionRelays(relayUrls)
|
|
// Fetch bookmark events - NIP-51 standards, legacy formats, and web bookmarks (NIP-B0)
|
|
console.log('🔍 Fetching bookmark events from relays:', relayUrls)
|
|
// Try local-first quickly, then full set fallback
|
|
const local$ = localRelays.length > 0
|
|
? relayPool
|
|
.req(localRelays, { kinds: [10003, 30003, 30001, 39701], authors: [activeAccount.pubkey] })
|
|
.pipe(completeOnEose(), takeUntil(timer(1200)))
|
|
: new Observable<NostrEvent>((sub) => sub.complete())
|
|
const remote$ = remoteRelays.length > 0
|
|
? relayPool
|
|
.req(remoteRelays, { kinds: [10003, 30003, 30001, 39701], authors: [activeAccount.pubkey] })
|
|
.pipe(completeOnEose(), takeUntil(timer(6000)))
|
|
: new Observable<NostrEvent>((sub) => sub.complete())
|
|
const rawEvents = await lastValueFrom(merge(local$, remote$).pipe(toArray()))
|
|
console.log('📊 Raw events fetched:', rawEvents.length, 'events')
|
|
|
|
// Rebroadcast bookmark events to local/all relays based on settings
|
|
await rebroadcastEvents(rawEvents, relayPool, settings)
|
|
|
|
// Check for events with potentially encrypted content
|
|
const eventsWithContent = rawEvents.filter(evt => evt.content && evt.content.length > 0)
|
|
if (eventsWithContent.length > 0) {
|
|
console.log('🔐 Events with content (potentially encrypted):', eventsWithContent.length)
|
|
eventsWithContent.forEach((evt, i) => {
|
|
const dTag = evt.tags?.find((t: string[]) => t[0] === 'd')?.[1] || 'none'
|
|
const contentPreview = evt.content.slice(0, 60) + (evt.content.length > 60 ? '...' : '')
|
|
console.log(` Encrypted Event ${i}: kind=${evt.kind}, id=${evt.id?.slice(0, 8)}, dTag=${dTag}, contentLength=${evt.content.length}, preview=${contentPreview}`)
|
|
})
|
|
}
|
|
|
|
rawEvents.forEach((evt, i) => {
|
|
const dTag = evt.tags?.find((t: string[]) => t[0] === 'd')?.[1] || 'none'
|
|
const contentPreview = evt.content ? evt.content.slice(0, 50) + (evt.content.length > 50 ? '...' : '') : 'empty'
|
|
console.log(` Event ${i}: kind=${evt.kind}, id=${evt.id?.slice(0, 8)}, dTag=${dTag}, contentLength=${evt.content?.length || 0}, contentPreview=${contentPreview}`)
|
|
})
|
|
|
|
const bookmarkListEvents = dedupeNip51Events(rawEvents)
|
|
console.log('📋 After deduplication:', bookmarkListEvents.length, 'bookmark events')
|
|
if (bookmarkListEvents.length === 0) {
|
|
// Keep existing bookmarks visible; do not clear list if nothing new found
|
|
return
|
|
}
|
|
// Aggregate across events
|
|
const maybeAccount = activeAccount as AccountWithExtension
|
|
console.log('🔐 Account object:', {
|
|
hasSignEvent: typeof maybeAccount?.signEvent === 'function',
|
|
hasSigner: !!maybeAccount?.signer,
|
|
accountType: typeof maybeAccount,
|
|
accountKeys: maybeAccount ? Object.keys(maybeAccount) : []
|
|
})
|
|
|
|
// For ExtensionAccount, we need a signer with nip04/nip44 for decrypting hidden content
|
|
// The ExtensionAccount itself has nip04/nip44 getters that proxy to the signer
|
|
let signerCandidate: unknown = maybeAccount
|
|
const hasNip04Prop = (signerCandidate as { nip04?: unknown })?.nip04 !== undefined
|
|
const hasNip44Prop = (signerCandidate as { nip44?: unknown })?.nip44 !== undefined
|
|
if (signerCandidate && !hasNip04Prop && !hasNip44Prop && maybeAccount?.signer) {
|
|
// Fallback to the raw signer if account doesn't have nip04/nip44
|
|
signerCandidate = maybeAccount.signer
|
|
}
|
|
|
|
console.log('🔑 Signer candidate:', !!signerCandidate, typeof signerCandidate)
|
|
if (signerCandidate) {
|
|
console.log('🔑 Signer has nip04:', hasNip04Decrypt(signerCandidate))
|
|
console.log('🔑 Signer has nip44:', hasNip44Decrypt(signerCandidate))
|
|
}
|
|
const { publicItemsAll, privateItemsAll, newestCreatedAt, latestContent, allTags } = await collectBookmarksFromEvents(
|
|
bookmarkListEvents,
|
|
activeAccount,
|
|
signerCandidate
|
|
)
|
|
|
|
const allItems = [...publicItemsAll, ...privateItemsAll]
|
|
const noteIds = Array.from(new Set(allItems.map(i => i.id).filter(isHexId)))
|
|
let idToEvent: Map<string, NostrEvent> = new Map()
|
|
if (noteIds.length > 0) {
|
|
try {
|
|
const { local: localHydrate, remote: remoteHydrate } = partitionRelays(relayUrls)
|
|
const localHydrate$ = localHydrate.length > 0
|
|
? relayPool.req(localHydrate, { ids: noteIds }).pipe(completeOnEose(), takeUntil(timer(800)))
|
|
: new Observable<NostrEvent>((sub) => sub.complete())
|
|
const remoteHydrate$ = remoteHydrate.length > 0
|
|
? relayPool.req(remoteHydrate, { ids: noteIds }).pipe(completeOnEose(), takeUntil(timer(2500)))
|
|
: new Observable<NostrEvent>((sub) => sub.complete())
|
|
const events: NostrEvent[] = await lastValueFrom(merge(localHydrate$, remoteHydrate$).pipe(toArray()))
|
|
idToEvent = new Map(events.map((e: NostrEvent) => [e.id, e]))
|
|
} catch (error) {
|
|
console.warn('Failed to fetch events for hydration:', error)
|
|
}
|
|
}
|
|
const allBookmarks = dedupeBookmarksById([
|
|
...hydrateItems(publicItemsAll, idToEvent),
|
|
...hydrateItems(privateItemsAll, idToEvent)
|
|
])
|
|
|
|
// Sort individual bookmarks by "added" timestamp first (most recently added first),
|
|
// falling back to event created_at when unknown.
|
|
const enriched = allBookmarks.map(b => ({
|
|
...b,
|
|
tags: b.tags || [],
|
|
content: b.content || ''
|
|
}))
|
|
const sortedBookmarks = enriched
|
|
.map(b => ({ ...b, urlReferences: extractUrlsFromContent(b.content) }))
|
|
.sort((a, b) => ((b.added_at || 0) - (a.added_at || 0)) || ((b.created_at || 0) - (a.created_at || 0)))
|
|
|
|
const bookmark: Bookmark = {
|
|
id: `${activeAccount.pubkey}-bookmarks`,
|
|
title: `Bookmarks (${sortedBookmarks.length})`,
|
|
url: '',
|
|
content: latestContent,
|
|
created_at: newestCreatedAt || Math.floor(Date.now() / 1000),
|
|
tags: allTags,
|
|
bookmarkCount: sortedBookmarks.length,
|
|
eventReferences: allTags.filter((tag: string[]) => tag[0] === 'e').map((tag: string[]) => tag[1]),
|
|
individualBookmarks: sortedBookmarks,
|
|
isPrivate: privateItemsAll.length > 0,
|
|
encryptedContent: undefined
|
|
}
|
|
|
|
setBookmarks([bookmark])
|
|
|
|
} catch (error) {
|
|
console.error('Failed to fetch bookmarks:', error)
|
|
}
|
|
} |