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 }