feat: add private bookmark fetching with NIP-44 decryption

- Update ActiveAccount interface to include signer property
- Modify fetchBookmarks to handle both kind 10003 (public) and kind 30001 (private) bookmark lists
- Implement NIP-44 decryption for private bookmarks using applesauce SimpleSigner
- Support both public tags and encrypted private content in categorized bookmarks
- Maintain backward compatibility with existing public bookmark functionality
This commit is contained in:
Gigi
2025-10-02 09:25:57 +02:00
parent e0acd2f7e7
commit 6e57c6227c
2 changed files with 148 additions and 66 deletions

View File

@@ -19,11 +19,11 @@ export const fetchBookmarks = async (
// Get relay URLs from the pool
const relayUrls = Array.from(relayPool.relays.values()).map(relay => relay.url)
// Step 1: Fetch the bookmark list event (kind 10003)
// Step 1: Fetch both public bookmark lists (kind 10003) and private bookmark lists (kind 30001)
const bookmarkListFilter: Filter = {
kinds: [10003],
kinds: [10003, 30001],
authors: [activeAccount.pubkey],
limit: 1 // Just get the most recent bookmark list
limit: 10 // Get multiple bookmark lists
}
console.log('Fetching bookmark list with filter:', bookmarkListFilter)
@@ -44,78 +44,159 @@ export const fetchBookmarks = async (
return
}
// Step 2: Extract event IDs from the bookmark list
const bookmarkListEvent = bookmarkListEvents[0]
const eventTags = bookmarkListEvent.tags.filter(tag => tag[0] === 'e')
const eventIds = eventTags.map(tag => tag[1])
// Step 2: Process each bookmark list event
const allBookmarks: Bookmark[] = []
console.log('Found event IDs in bookmark list:', eventIds.length, eventIds)
if (eventIds.length === 0) {
console.log('No event references found in bookmark list')
setBookmarks([])
setLoading(false)
return
}
// Step 3: Fetch each individual event
console.log('Fetching individual events...')
const individualBookmarks: IndividualBookmark[] = []
for (const eventId of eventIds) {
try {
console.log('Fetching event:', eventId)
const eventFilter: Filter = {
ids: [eventId]
}
for (const bookmarkListEvent of bookmarkListEvents) {
console.log('Processing bookmark list event:', bookmarkListEvent.id, 'kind:', bookmarkListEvent.kind)
if (bookmarkListEvent.kind === 10003) {
// Handle public bookmark lists (existing logic)
const eventTags = bookmarkListEvent.tags.filter(tag => tag[0] === 'e')
const eventIds = eventTags.map(tag => tag[1])
const events = await lastValueFrom(
relayPool.req(relayUrls, eventFilter).pipe(
completeOnEose(),
takeUntil(timer(5000)),
toArray(),
)
)
console.log('Found event IDs in public bookmark list:', eventIds.length, eventIds)
if (events.length > 0) {
const event = events[0]
const parsedContent = event.content ? getParsedContent(event.content) as ParsedContent : undefined
if (eventIds.length > 0) {
// Fetch individual events for public bookmarks
const individualBookmarks: IndividualBookmark[] = []
individualBookmarks.push({
id: event.id,
content: event.content,
created_at: event.created_at,
pubkey: event.pubkey,
kind: event.kind,
tags: event.tags,
parsedContent: parsedContent,
type: 'event'
})
console.log('Successfully fetched event:', event.id)
} else {
console.log('Event not found:', eventId)
for (const eventId of eventIds) {
try {
console.log('Fetching public event:', eventId)
const eventFilter: Filter = {
ids: [eventId]
}
const events = await lastValueFrom(
relayPool.req(relayUrls, eventFilter).pipe(
completeOnEose(),
takeUntil(timer(5000)),
toArray(),
)
)
if (events.length > 0) {
const event = events[0]
const parsedContent = event.content ? getParsedContent(event.content) as ParsedContent : undefined
individualBookmarks.push({
id: event.id,
content: event.content,
created_at: event.created_at,
pubkey: event.pubkey,
kind: event.kind,
tags: event.tags,
parsedContent: parsedContent,
type: 'event'
})
console.log('Successfully fetched public event:', event.id)
}
} catch (error) {
console.error('Error fetching public event:', eventId, error)
}
}
const bookmark: Bookmark = {
id: bookmarkListEvent.id,
title: bookmarkListEvent.content || `Public Bookmarks (${individualBookmarks.length} items)`,
url: '',
content: bookmarkListEvent.content,
created_at: bookmarkListEvent.created_at,
tags: bookmarkListEvent.tags,
bookmarkCount: individualBookmarks.length,
eventReferences: eventIds,
individualBookmarks: individualBookmarks
}
allBookmarks.push(bookmark)
}
} else if (bookmarkListEvent.kind === 30001) {
// Handle private bookmark lists (NIP-51)
console.log('Processing private bookmark list:', bookmarkListEvent.id)
try {
// Extract public bookmarks from tags
const publicBookmarks: IndividualBookmark[] = []
const publicTags = bookmarkListEvent.tags.filter(tag =>
tag[0] === 'r' || tag[0] === 'e' || tag[0] === 'a'
)
for (const tag of publicTags) {
if (tag[0] === 'r' && tag[1]) {
// URL bookmark
publicBookmarks.push({
id: `${bookmarkListEvent.id}-${tag[1]}`,
content: tag[2] || tag[1],
created_at: bookmarkListEvent.created_at,
pubkey: bookmarkListEvent.pubkey,
kind: bookmarkListEvent.kind,
tags: [tag],
type: 'article'
})
}
}
// Decrypt private bookmarks from content
let privateBookmarks: IndividualBookmark[] = []
if (bookmarkListEvent.content && activeAccount.signer) {
try {
console.log('Decrypting private bookmarks...')
const decryptedContent = await activeAccount.signer.nip44_decrypt(
bookmarkListEvent.content,
activeAccount.pubkey
)
console.log('Decrypted content:', decryptedContent)
// Parse the decrypted JSON content
const privateTags = JSON.parse(decryptedContent)
for (const tag of privateTags) {
if (tag[0] === 'r' && tag[1]) {
// Private URL bookmark
privateBookmarks.push({
id: `${bookmarkListEvent.id}-private-${tag[1]}`,
content: tag[2] || tag[1],
created_at: bookmarkListEvent.created_at,
pubkey: bookmarkListEvent.pubkey,
kind: bookmarkListEvent.kind,
tags: [tag],
type: 'article'
})
}
}
console.log('Decrypted private bookmarks:', privateBookmarks.length)
} catch (decryptError) {
console.error('Error decrypting private bookmarks:', decryptError)
}
}
const allPrivateBookmarks = [...publicBookmarks, ...privateBookmarks]
const bookmark: Bookmark = {
id: bookmarkListEvent.id,
title: bookmarkListEvent.content || `Private Bookmarks (${allPrivateBookmarks.length} items)`,
url: '',
content: bookmarkListEvent.content,
created_at: bookmarkListEvent.created_at,
tags: bookmarkListEvent.tags,
bookmarkCount: allPrivateBookmarks.length,
individualBookmarks: allPrivateBookmarks
}
allBookmarks.push(bookmark)
} catch (error) {
console.error('Error processing private bookmark list:', error)
}
} catch (error) {
console.error('Error fetching event:', eventId, error)
}
}
console.log('Fetched individual bookmarks:', individualBookmarks.length)
console.log('Fetched all bookmarks:', allBookmarks.length)
// Create a single bookmark entry with all individual bookmarks
const bookmark: Bookmark = {
id: bookmarkListEvent.id,
title: bookmarkListEvent.content || `Bookmark List (${individualBookmarks.length} items)`,
url: '',
content: bookmarkListEvent.content,
created_at: bookmarkListEvent.created_at,
tags: bookmarkListEvent.tags,
bookmarkCount: individualBookmarks.length,
eventReferences: eventIds,
individualBookmarks: individualBookmarks
}
setBookmarks([bookmark])
setBookmarks(allBookmarks)
clearTimeout(timeoutId)
setLoading(false)

View File

@@ -40,4 +40,5 @@ export interface IndividualBookmark {
export interface ActiveAccount {
pubkey: string
signer?: any // SimpleSigner from applesauce
}