github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/storage/reacjis.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/types" 11 "github.com/keybase/client/go/chat/utils" 12 "github.com/keybase/client/go/encrypteddb" 13 "github.com/keybase/client/go/libkb" 14 "github.com/keybase/client/go/protocol/chat1" 15 "github.com/keybase/client/go/protocol/gregor1" 16 "github.com/keybase/client/go/protocol/keybase1" 17 "github.com/kyokomi/emoji" 18 context "golang.org/x/net/context" 19 ) 20 21 func init() { 22 // Don't add padding between emojis, we want skin tones to be rendered 23 // correctly. 24 emoji.ReplacePadding = "" 25 } 26 27 const ( 28 reacjiDiskVersion = 3 29 ) 30 31 // If the user has less than 5 favorite reacjis we stuff these defaults in. 32 var DefaultTopReacjis = []keybase1.UserReacji{ 33 {Name: ":+1:"}, 34 {Name: ":-1:"}, 35 {Name: ":joy:"}, 36 {Name: ":sunglasses:"}, 37 {Name: ":tada:"}, 38 } 39 40 func EmojiAliasList(shortCode string) []string { 41 return emojiRevCodeMap[emojiCodeMap[shortCode]] 42 } 43 44 // EmojiHasAlias flags if the given `shortCode` has multiple aliases with other 45 // codes. 46 func EmojiHasAlias(shortCode string) bool { 47 return len(EmojiAliasList(shortCode)) > 1 48 } 49 50 // EmojiExists flags if the given `shortCode` is a valid emoji 51 func EmojiExists(shortCode string) bool { 52 return len(EmojiAliasList(shortCode)) > 0 53 } 54 55 // NormalizeShortCode normalizes a given `shortCode` to a deterministic alias. 56 func NormalizeShortCode(shortCode string) string { 57 shortLists := EmojiAliasList(shortCode) 58 if len(shortLists) == 0 { 59 return shortCode 60 } 61 return shortLists[0] 62 } 63 64 type ReacjiInternalStorage struct { 65 FrequencyMap map[string]int 66 MtimeMap map[string]gregor1.Time 67 SkinTone keybase1.ReacjiSkinTone 68 } 69 70 func NewReacjiInternalStorage() ReacjiInternalStorage { 71 return ReacjiInternalStorage{ 72 FrequencyMap: make(map[string]int), 73 MtimeMap: make(map[string]gregor1.Time), 74 } 75 } 76 77 type reacjiMemCacheImpl struct { 78 sync.RWMutex 79 80 uid gregor1.UID 81 data ReacjiInternalStorage 82 } 83 84 func newReacjiMemCacheImpl() *reacjiMemCacheImpl { 85 return &reacjiMemCacheImpl{ 86 data: NewReacjiInternalStorage(), 87 } 88 } 89 90 func (i *reacjiMemCacheImpl) Get(uid gregor1.UID) (bool, ReacjiInternalStorage) { 91 i.RLock() 92 defer i.RUnlock() 93 if !uid.Eq(i.uid) { 94 return false, NewReacjiInternalStorage() 95 } 96 return true, i.data 97 } 98 99 func (i *reacjiMemCacheImpl) Put(uid gregor1.UID, data ReacjiInternalStorage) { 100 i.Lock() 101 defer i.Unlock() 102 i.uid = uid 103 i.data = data 104 } 105 106 func (i *reacjiMemCacheImpl) clearMemCaches() { 107 i.Lock() 108 defer i.Unlock() 109 i.data = NewReacjiInternalStorage() 110 i.uid = nil 111 } 112 113 func (i *reacjiMemCacheImpl) OnLogout(mctx libkb.MetaContext) error { 114 i.clearMemCaches() 115 return nil 116 } 117 118 func (i *reacjiMemCacheImpl) OnDbNuke(mctx libkb.MetaContext) error { 119 i.clearMemCaches() 120 return nil 121 } 122 123 var reacjiMemCache = newReacjiMemCacheImpl() 124 125 type reacjiPair struct { 126 name string 127 score float64 128 freq int 129 } 130 131 func newReacjiPair(name string, freq int, score float64) reacjiPair { 132 return reacjiPair{ 133 name: name, 134 freq: freq, 135 score: score, 136 } 137 } 138 139 type reacjiDiskEntry struct { 140 Version int 141 // reacji name -> frequency 142 Data ReacjiInternalStorage 143 } 144 145 type ReacjiStore struct { 146 globals.Contextified 147 sync.Mutex 148 utils.DebugLabeler 149 150 encryptedDB *encrypteddb.EncryptedDB 151 } 152 153 // Keeps map counting emoji used in reactions for each user. Used to populate 154 // the reacji heads up display. 155 // Data is stored in an encrypted leveldb in the form: 156 // 157 // uid -> { 158 // reacjiName: frequency, 159 // ":+1:": 5, 160 // ... 161 // }, 162 func NewReacjiStore(g *globals.Context) *ReacjiStore { 163 keyFn := func(ctx context.Context) ([32]byte, error) { 164 return GetSecretBoxKey(ctx, g.ExternalG()) 165 } 166 dbFn := func(g *libkb.GlobalContext) *libkb.JSONLocalDb { 167 return g.LocalChatDb 168 } 169 return &ReacjiStore{ 170 Contextified: globals.NewContextified(g), 171 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "ReacjiStore", false), 172 encryptedDB: encrypteddb.New(g.ExternalG(), dbFn, keyFn), 173 } 174 } 175 176 func (s *ReacjiStore) dbKey(uid gregor1.UID) libkb.DbKey { 177 return libkb.DbKey{ 178 Typ: libkb.DBChatReacji, 179 Key: fmt.Sprintf("ri:%s", uid), 180 } 181 } 182 183 func (s *ReacjiStore) populateCacheLocked(ctx context.Context, uid gregor1.UID) (cache ReacjiInternalStorage) { 184 if found, cache := reacjiMemCache.Get(uid); found { 185 return cache 186 } 187 188 // populate the cache after we fetch from disk 189 cache = NewReacjiInternalStorage() 190 defer func() { reacjiMemCache.Put(uid, cache) }() 191 192 dbKey := s.dbKey(uid) 193 var entry reacjiDiskEntry 194 found, err := s.encryptedDB.Get(ctx, dbKey, &entry) 195 if err != nil || !found { 196 s.Debug(ctx, "reacji map not found on disk") 197 return cache 198 } 199 200 if entry.Version != reacjiDiskVersion { 201 // drop the history if our format changed 202 s.Debug(ctx, "Deleting reacjiCache found version %d, current version %d", entry.Version, reacjiDiskVersion) 203 if err = s.encryptedDB.Delete(ctx, dbKey); err != nil { 204 s.Debug(ctx, "unable to delete cache entry: %v", err) 205 } 206 return cache 207 } 208 209 if entry.Data.FrequencyMap == nil { 210 entry.Data.FrequencyMap = make(map[string]int) 211 } 212 if entry.Data.MtimeMap == nil { 213 entry.Data.MtimeMap = make(map[string]gregor1.Time) 214 } 215 216 cache = entry.Data 217 // Normalized duplicated aliases 218 for name, freq := range cache.FrequencyMap { 219 normalized := NormalizeShortCode(name) 220 if name != normalized { 221 cache.FrequencyMap[normalized] += freq 222 if cache.MtimeMap[name] > cache.MtimeMap[normalized] { 223 cache.MtimeMap[normalized] = cache.MtimeMap[name] 224 } 225 delete(cache.FrequencyMap, name) 226 delete(cache.MtimeMap, name) 227 } 228 } 229 return cache 230 } 231 232 func (s *ReacjiStore) PutReacji(ctx context.Context, uid gregor1.UID, shortCode string) error { 233 s.Lock() 234 defer s.Unlock() 235 if !(EmojiHasAlias(shortCode) || globals.EmojiPattern.MatchString(shortCode)) { 236 return nil 237 } 238 cache := s.populateCacheLocked(ctx, uid) 239 shortCode = NormalizeShortCode(shortCode) 240 cache.FrequencyMap[shortCode]++ 241 cache.MtimeMap[shortCode] = gregor1.ToTime(time.Now()) 242 243 dbKey := s.dbKey(uid) 244 err := s.encryptedDB.Put(ctx, dbKey, reacjiDiskEntry{ 245 Version: reacjiDiskVersion, 246 Data: cache, 247 }) 248 if err != nil { 249 return err 250 } 251 reacjiMemCache.Put(uid, cache) 252 return nil 253 } 254 255 func (s *ReacjiStore) PutSkinTone(ctx context.Context, uid gregor1.UID, 256 skinTone keybase1.ReacjiSkinTone) error { 257 s.Lock() 258 defer s.Unlock() 259 260 if skinTone > 5 { 261 skinTone = 0 262 } 263 264 cache := s.populateCacheLocked(ctx, uid) 265 cache.SkinTone = skinTone 266 dbKey := s.dbKey(uid) 267 err := s.encryptedDB.Put(ctx, dbKey, reacjiDiskEntry{ 268 Version: reacjiDiskVersion, 269 Data: cache, 270 }) 271 if err != nil { 272 return err 273 } 274 reacjiMemCache.Put(uid, cache) 275 return nil 276 } 277 278 func (s *ReacjiStore) GetInternalStore(ctx context.Context, uid gregor1.UID) ReacjiInternalStorage { 279 s.Lock() 280 defer s.Unlock() 281 return s.populateCacheLocked(ctx, uid) 282 } 283 284 // UserReacjis returns the user's most frequently used reacjis falling back to 285 // `DefaultTopReacjis` if there is not enough history. Results are ordered by 286 // frequency and then alphabetically. 287 func (s *ReacjiStore) UserReacjis(ctx context.Context, uid gregor1.UID) keybase1.UserReacjis { 288 s.Lock() 289 defer s.Unlock() 290 291 customMap := make(map[string]string) 292 customMapNoAnim := make(map[string]string) 293 cache := s.populateCacheLocked(ctx, uid) 294 // resolve custom emoji 295 for name := range cache.FrequencyMap { 296 if s.G().EmojiSource.IsStockEmoji(name) { 297 continue 298 } 299 harvested, err := s.G().EmojiSource.Harvest(ctx, name, uid, chat1.ConversationID{}, 300 types.EmojiHarvestModeFast) 301 if err != nil { 302 s.Debug(ctx, "UserReacjis: failed to harvest possible custom: %s", err) 303 delete(cache.FrequencyMap, name) 304 continue 305 } 306 if len(harvested) == 0 { 307 s.Debug(ctx, "UserReacjis: no harvest results for possible custom") 308 delete(cache.FrequencyMap, name) 309 continue 310 } 311 source, noAnimSource, err := s.G().EmojiSource.RemoteToLocalSource(ctx, uid, harvested[0].Source) 312 if err != nil { 313 s.Debug(ctx, "UserReacjis: failed to convert to local source: %s", err) 314 delete(cache.FrequencyMap, name) 315 continue 316 } 317 if !source.IsHTTPSrv() || !noAnimSource.IsHTTPSrv() { 318 s.Debug(ctx, "UserReacjis: not http srv source") 319 delete(cache.FrequencyMap, name) 320 continue 321 } 322 customMap[name] = source.Httpsrv() 323 customMapNoAnim[name] = noAnimSource.Httpsrv() 324 } 325 326 // add defaults if needed so we always return some values 327 for _, el := range DefaultTopReacjis { 328 if len(cache.FrequencyMap) >= len(DefaultTopReacjis) { 329 break 330 } 331 if _, ok := cache.FrequencyMap[el.Name]; !ok { 332 cache.FrequencyMap[el.Name] = 0 333 cache.MtimeMap[el.Name] = 0 334 } 335 } 336 337 pairs := make([]reacjiPair, 0, len(cache.FrequencyMap)) 338 for name, freq := range cache.FrequencyMap { 339 mtime := cache.MtimeMap[name] 340 score := ScoreByFrequencyAndMtime(freq, mtime) 341 pairs = append(pairs, newReacjiPair(name, freq, score)) 342 } 343 // sort by frequency and then alphabetically 344 sort.Slice(pairs, func(i, j int) bool { 345 if pairs[i].score == pairs[j].score { 346 return pairs[i].name < pairs[j].name 347 } 348 return pairs[i].score > pairs[j].score 349 }) 350 reacjis := make([]keybase1.UserReacji, 0, len(pairs)) 351 for _, p := range pairs { 352 if len(reacjis) >= len(DefaultTopReacjis) && p.freq == 0 { 353 delete(cache.FrequencyMap, p.name) 354 delete(cache.MtimeMap, p.name) 355 } else { 356 reacji := keybase1.UserReacji{ 357 Name: p.name, 358 } 359 if addr, ok := customMap[p.name]; ok { 360 reacji.CustomAddr = &addr 361 } 362 if addr, ok := customMapNoAnim[p.name]; ok { 363 reacji.CustomAddrNoAnim = &addr 364 } 365 reacjis = append(reacjis, reacji) 366 } 367 } 368 369 return keybase1.UserReacjis{ 370 TopReacjis: reacjis, 371 SkinTone: cache.SkinTone, 372 } 373 }