From 6e57c6227c9e141b86d95580da07c2ff5bc95ebe Mon Sep 17 00:00:00 2001 From: Gigi Date: Thu, 2 Oct 2025 09:25:57 +0200 Subject: [PATCH] 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 --- src/services/bookmarkService.ts | 213 ++++++++++++++++++++++---------- src/types/bookmarks.ts | 1 + 2 files changed, 148 insertions(+), 66 deletions(-) diff --git a/src/services/bookmarkService.ts b/src/services/bookmarkService.ts index 32c9c690..d51ff227 100644 --- a/src/services/bookmarkService.ts +++ b/src/services/bookmarkService.ts @@ -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) diff --git a/src/types/bookmarks.ts b/src/types/bookmarks.ts index 8ad884b0..5ea486fe 100644 --- a/src/types/bookmarks.ts +++ b/src/types/bookmarks.ts @@ -40,4 +40,5 @@ export interface IndividualBookmark { export interface ActiveAccount { pubkey: string + signer?: any // SimpleSigner from applesauce }