github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/storage/giphy.go (about)

     1  package storage
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/keybase/client/go/chat/globals"
    10  	"github.com/keybase/client/go/chat/utils"
    11  	"github.com/keybase/client/go/encrypteddb"
    12  	"github.com/keybase/client/go/libkb"
    13  	"github.com/keybase/client/go/protocol/chat1"
    14  	"github.com/keybase/client/go/protocol/gregor1"
    15  	context "golang.org/x/net/context"
    16  )
    17  
    18  const (
    19  	giphyDiskVersion = 2
    20  )
    21  
    22  // track frequency/mtime of giphy search results that are sent.
    23  type GiphyResultFrequency struct {
    24  	Result chat1.GiphySearchResult
    25  	Count  int
    26  	Mtime  gregor1.Time
    27  }
    28  
    29  // Locally track the usage of particular giphy images to power the UI.
    30  type GiphyInternalStorage struct {
    31  	// targetURL -> result frequency/mtime
    32  	Results map[string]GiphyResultFrequency
    33  }
    34  
    35  func NewGiphyInternalStorage() GiphyInternalStorage {
    36  	return GiphyInternalStorage{
    37  		Results: make(map[string]GiphyResultFrequency),
    38  	}
    39  }
    40  
    41  type giphyMemCacheImpl struct {
    42  	sync.RWMutex
    43  
    44  	uid  gregor1.UID
    45  	data GiphyInternalStorage
    46  }
    47  
    48  func newGiphyMemCacheImpl() *giphyMemCacheImpl {
    49  	return &giphyMemCacheImpl{
    50  		data: NewGiphyInternalStorage(),
    51  	}
    52  }
    53  
    54  func (i *giphyMemCacheImpl) Get(uid gregor1.UID) (bool, GiphyInternalStorage) {
    55  	i.RLock()
    56  	defer i.RUnlock()
    57  	if !uid.Eq(i.uid) {
    58  		return false, NewGiphyInternalStorage()
    59  	}
    60  	return true, i.data
    61  }
    62  
    63  func (i *giphyMemCacheImpl) Put(uid gregor1.UID, data GiphyInternalStorage) {
    64  	i.Lock()
    65  	defer i.Unlock()
    66  	i.uid = uid
    67  	i.data = data
    68  }
    69  
    70  func (i *giphyMemCacheImpl) clearMemCaches() {
    71  	i.Lock()
    72  	defer i.Unlock()
    73  	i.data = NewGiphyInternalStorage()
    74  	i.uid = nil
    75  }
    76  
    77  func (i *giphyMemCacheImpl) OnLogout(mctx libkb.MetaContext) error {
    78  	i.clearMemCaches()
    79  	return nil
    80  }
    81  
    82  func (i *giphyMemCacheImpl) OnDbNuke(mctx libkb.MetaContext) error {
    83  	i.clearMemCaches()
    84  	return nil
    85  }
    86  
    87  var giphyMemCache = newGiphyMemCacheImpl()
    88  
    89  type giphyDiskEntry struct {
    90  	Version int
    91  	Data    GiphyInternalStorage
    92  }
    93  
    94  type GiphyStore struct {
    95  	globals.Contextified
    96  	sync.Mutex
    97  	utils.DebugLabeler
    98  
    99  	encryptedDB *encrypteddb.EncryptedDB
   100  }
   101  
   102  // Keeps map counting giphy send, partitioned by user. Used to populate
   103  // the giphy default display/command HUD.
   104  // Data is stored in an encrypted leveldb in the form:
   105  //
   106  //		uid -> {
   107  //		         {
   108  //	               targetUrl: {GiphyResult, frequency, mtime},
   109  //	               ...
   110  //	             },
   111  //		},
   112  func NewGiphyStore(g *globals.Context) *GiphyStore {
   113  	keyFn := func(ctx context.Context) ([32]byte, error) {
   114  		return GetSecretBoxKey(ctx, g.ExternalG())
   115  	}
   116  	dbFn := func(g *libkb.GlobalContext) *libkb.JSONLocalDb {
   117  		return g.LocalChatDb
   118  	}
   119  	return &GiphyStore{
   120  		Contextified: globals.NewContextified(g),
   121  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "GiphyStore", false),
   122  		encryptedDB:  encrypteddb.New(g.ExternalG(), dbFn, keyFn),
   123  	}
   124  }
   125  
   126  func (s *GiphyStore) dbKey(uid gregor1.UID) libkb.DbKey {
   127  	return libkb.DbKey{
   128  		Typ: libkb.DBChatGiphy,
   129  		Key: fmt.Sprintf("gi:%s", uid),
   130  	}
   131  }
   132  
   133  func (s *GiphyStore) populateCacheLocked(ctx context.Context, uid gregor1.UID) (cache GiphyInternalStorage) {
   134  	if found, cache := giphyMemCache.Get(uid); found {
   135  		return cache
   136  	}
   137  
   138  	// populate the cache after we fetch from disk
   139  	cache = NewGiphyInternalStorage()
   140  	defer func() { giphyMemCache.Put(uid, cache) }()
   141  
   142  	dbKey := s.dbKey(uid)
   143  	var entry giphyDiskEntry
   144  	found, err := s.encryptedDB.Get(ctx, dbKey, &entry)
   145  	if err != nil || !found {
   146  		s.Debug(ctx, "giphy map not found on disk")
   147  		return cache
   148  	}
   149  
   150  	if entry.Version != giphyDiskVersion {
   151  		// drop the history if our format changed
   152  		s.Debug(ctx, "Deleting giphyCache found version %d, current version %d", entry.Version, reacjiDiskVersion)
   153  		if err = s.encryptedDB.Delete(ctx, dbKey); err != nil {
   154  			s.Debug(ctx, "unable to delete cache entry: %v", err)
   155  		}
   156  		return cache
   157  	}
   158  
   159  	if entry.Data.Results == nil {
   160  		entry.Data.Results = make(map[string]GiphyResultFrequency)
   161  	}
   162  
   163  	cache = entry.Data
   164  	return cache
   165  }
   166  
   167  func (s *GiphyStore) Put(ctx context.Context, uid gregor1.UID, giphy chat1.GiphySearchResult) error {
   168  	s.Lock()
   169  	defer s.Unlock()
   170  	cache := s.populateCacheLocked(ctx, uid)
   171  	resultItem, ok := cache.Results[giphy.TargetUrl]
   172  	if !ok {
   173  		resultItem.Result = giphy
   174  	}
   175  	resultItem.Count++
   176  	resultItem.Mtime = gregor1.ToTime(time.Now())
   177  	cache.Results[giphy.TargetUrl] = resultItem
   178  
   179  	dbKey := s.dbKey(uid)
   180  	err := s.encryptedDB.Put(ctx, dbKey, giphyDiskEntry{
   181  		Version: giphyDiskVersion,
   182  		Data:    cache,
   183  	})
   184  	if err != nil {
   185  		return err
   186  	}
   187  	giphyMemCache.Put(uid, cache)
   188  	return nil
   189  }
   190  
   191  type giphyFrequencyResultWithScore struct {
   192  	result GiphyResultFrequency
   193  	score  float64
   194  }
   195  
   196  // GiphyResults returns the user's most frequently used giphy results.
   197  // Results are ordered by frequency and then alphabetically but may be empty
   198  func (s *GiphyStore) GiphyResults(ctx context.Context, uid gregor1.UID, limit int) []chat1.GiphySearchResult {
   199  	s.Lock()
   200  	defer s.Unlock()
   201  
   202  	cache := s.populateCacheLocked(ctx, uid)
   203  
   204  	pairs := make([]giphyFrequencyResultWithScore, 0, len(cache.Results))
   205  	for _, res := range cache.Results {
   206  		score := ScoreByFrequencyAndMtime(res.Count, res.Mtime)
   207  		pairs = append(pairs, giphyFrequencyResultWithScore{result: res, score: score})
   208  	}
   209  	// sort by frequency and then alphabetically
   210  	sort.Slice(pairs, func(i, j int) bool {
   211  		if pairs[i].score == pairs[j].score {
   212  			return pairs[i].result.Result.TargetUrl < pairs[j].result.Result.TargetUrl
   213  		}
   214  		return pairs[i].score > pairs[j].score
   215  	})
   216  	if len(pairs) > limit {
   217  		pairs = pairs[:limit]
   218  	}
   219  	results := make([]chat1.GiphySearchResult, 0, len(pairs))
   220  	for _, p := range pairs {
   221  		results = append(results, p.result.Result)
   222  	}
   223  	return results
   224  }