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