PinnedUsersListRepositoryImpl.ts raw
1 import { PinnedUsersListRepository, PinnedUsersList, Pubkey, tryToPinnedUsersList } from '@/domain'
2 import { ExtendedKind } from '@/constants'
3 import client from '@/services/client.service'
4 import indexedDb from '@/services/indexed-db.service'
5 import { Event as NostrEvent } from 'nostr-tools'
6 import { RepositoryDependencies } from './types'
7
8 /**
9 * Function to decrypt private pins (NIP-04)
10 */
11 export type DecryptFn = (ciphertext: string, pubkey: string) => Promise<string>
12
13 /**
14 * Function to encrypt private pins (NIP-04)
15 */
16 export type EncryptFn = (plaintext: string, pubkey: string) => Promise<string>
17
18 /**
19 * Dependencies for PinnedUsersList repository
20 */
21 export interface PinnedUsersListRepositoryDependencies extends RepositoryDependencies {
22 /**
23 * NIP-04 decrypt function for private pins
24 */
25 decrypt: DecryptFn
26
27 /**
28 * NIP-04 encrypt function for private pins
29 */
30 encrypt: EncryptFn
31
32 /**
33 * The current user's pubkey (for encryption/decryption)
34 */
35 currentUserPubkey: string
36 }
37
38 /**
39 * IndexedDB + Relay implementation of PinnedUsersListRepository
40 *
41 * Uses IndexedDB for local caching and the client service for relay fetching.
42 * Handles NIP-04 encryption/decryption for private pins.
43 */
44 export class PinnedUsersListRepositoryImpl implements PinnedUsersListRepository {
45 constructor(private readonly deps: PinnedUsersListRepositoryDependencies) {}
46
47 async findByOwner(pubkey: Pubkey): Promise<PinnedUsersList | null> {
48 // Try cache first
49 const cachedEvent = await indexedDb.getReplaceableEvent(pubkey.hex, ExtendedKind.PINNED_USERS)
50 let event = cachedEvent
51
52 // Fetch from relays if not cached
53 if (!event) {
54 event = await client.fetchPinnedUsersList(pubkey.hex)
55 }
56
57 if (!event) return null
58
59 // Create the aggregate from the event
60 const pinnedUsersList = tryToPinnedUsersList(event)
61 if (!pinnedUsersList) return null
62
63 // Decrypt private pins if this is the current user's list
64 if (event.pubkey === this.deps.currentUserPubkey && event.content) {
65 try {
66 // Try to get decrypted content from cache
67 const cacheKey = `pinned:${event.id}`
68 let decryptedContent = await indexedDb.getDecryptedContent(cacheKey)
69
70 if (!decryptedContent) {
71 decryptedContent = await this.deps.decrypt(event.content, event.pubkey)
72 await indexedDb.putDecryptedContent(cacheKey, decryptedContent)
73 }
74
75 const privateTags = JSON.parse(decryptedContent)
76 pinnedUsersList.setPrivatePins(privateTags)
77 } catch {
78 // Decryption failed, proceed with empty private pins
79 }
80 }
81
82 return pinnedUsersList
83 }
84
85 async save(pinnedUsersList: PinnedUsersList): Promise<void> {
86 // Encrypt private pins
87 const privateTags = pinnedUsersList.toPrivateTags()
88 let encryptedContent = ''
89
90 if (privateTags.length > 0) {
91 const plaintext = JSON.stringify(privateTags)
92 encryptedContent = await this.deps.encrypt(plaintext, this.deps.currentUserPubkey)
93 }
94
95 // Set encrypted content on the aggregate before creating draft
96 pinnedUsersList.setEncryptedContent(encryptedContent)
97
98 const draftEvent = pinnedUsersList.toDraftEvent()
99 const publishedEvent = await this.deps.publish(draftEvent)
100
101 // Update cache
102 await indexedDb.putReplaceableEvent(publishedEvent)
103
104 // Cache the decrypted content
105 if (encryptedContent) {
106 const cacheKey = `pinned:${publishedEvent.id}`
107 await indexedDb.putDecryptedContent(cacheKey, JSON.stringify(privateTags))
108 }
109 }
110
111 /**
112 * Save and return the published event (for UI state updates)
113 */
114 async saveAndGetEvent(pinnedUsersList: PinnedUsersList): Promise<{ event: NostrEvent; privateTags: string[][] }> {
115 const privateTags = pinnedUsersList.toPrivateTags()
116 let encryptedContent = ''
117
118 if (privateTags.length > 0) {
119 const plaintext = JSON.stringify(privateTags)
120 encryptedContent = await this.deps.encrypt(plaintext, this.deps.currentUserPubkey)
121 }
122
123 pinnedUsersList.setEncryptedContent(encryptedContent)
124
125 const draftEvent = pinnedUsersList.toDraftEvent()
126 const publishedEvent = await this.deps.publish(draftEvent)
127
128 await indexedDb.putReplaceableEvent(publishedEvent)
129
130 if (encryptedContent) {
131 const cacheKey = `pinned:${publishedEvent.id}`
132 await indexedDb.putDecryptedContent(cacheKey, JSON.stringify(privateTags))
133 }
134
135 return { event: publishedEvent, privateTags }
136 }
137 }
138