MuteListRepositoryImpl.ts raw
1 import { MuteListRepository, MuteList, Pubkey, tryToMuteList } from '@/domain'
2 import client from '@/services/client.service'
3 import indexedDb from '@/services/indexed-db.service'
4 import { kinds } from 'nostr-tools'
5 import { RepositoryDependencies } from './types'
6
7 /**
8 * Function to decrypt private mutes (NIP-04)
9 */
10 export type DecryptFn = (ciphertext: string, pubkey: string) => Promise<string>
11
12 /**
13 * Function to encrypt private mutes (NIP-04)
14 */
15 export type EncryptFn = (plaintext: string, pubkey: string) => Promise<string>
16
17 /**
18 * Extended dependencies for MuteList repository
19 */
20 export interface MuteListRepositoryDependencies extends RepositoryDependencies {
21 /**
22 * NIP-04 decrypt function for private mutes
23 */
24 decrypt: DecryptFn
25
26 /**
27 * NIP-04 encrypt function for private mutes
28 */
29 encrypt: EncryptFn
30
31 /**
32 * The current user's pubkey (for encryption/decryption)
33 */
34 currentUserPubkey: string
35 }
36
37 /**
38 * IndexedDB + Relay implementation of MuteListRepository
39 *
40 * Uses IndexedDB for local caching and the client service for relay fetching.
41 * Handles NIP-04 encryption/decryption for private mutes.
42 */
43 export class MuteListRepositoryImpl implements MuteListRepository {
44 constructor(private readonly deps: MuteListRepositoryDependencies) {}
45
46 async findByOwner(pubkey: Pubkey): Promise<MuteList | null> {
47 // Try cache first
48 const cachedEvent = await indexedDb.getReplaceableEvent(pubkey.hex, kinds.Mutelist)
49 let event = cachedEvent
50
51 // Fetch from relays if not cached
52 if (!event) {
53 event = await client.fetchMuteListEvent(pubkey.hex)
54 }
55
56 if (!event) return null
57
58 // Decrypt private mutes if this is the current user's mute list
59 let privateTags: string[][] = []
60 if (event.pubkey === this.deps.currentUserPubkey && event.content) {
61 try {
62 // Try to get decrypted content from cache
63 const cacheKey = `mute:${event.id}`
64 let decryptedContent = await indexedDb.getDecryptedContent(cacheKey)
65
66 if (!decryptedContent) {
67 decryptedContent = await this.deps.decrypt(event.content, event.pubkey)
68 await indexedDb.putDecryptedContent(cacheKey, decryptedContent)
69 }
70
71 privateTags = JSON.parse(decryptedContent)
72 } catch {
73 // Decryption failed, proceed with empty private tags
74 }
75 }
76
77 return tryToMuteList(event, privateTags)
78 }
79
80 async save(muteList: MuteList): Promise<void> {
81 // Encrypt private mutes
82 const privateTags = muteList.toPrivateTags()
83 let encryptedContent = ''
84
85 if (privateTags.length > 0) {
86 const plaintext = JSON.stringify(privateTags)
87 encryptedContent = await this.deps.encrypt(plaintext, this.deps.currentUserPubkey)
88 }
89
90 const draftEvent = muteList.toDraftEvent(encryptedContent)
91 const publishedEvent = await this.deps.publish(draftEvent)
92
93 // Update cache
94 await indexedDb.putReplaceableEvent(publishedEvent)
95
96 // Cache the decrypted content
97 if (encryptedContent) {
98 const cacheKey = `mute:${publishedEvent.id}`
99 await indexedDb.putDecryptedContent(cacheKey, JSON.stringify(privateTags))
100 }
101 }
102 }
103