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  }