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

     1  package chat
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"image/gif"
     8  	"io"
     9  	"net/http"
    10  	"os"
    11  	"sort"
    12  	"strings"
    13  	"sync"
    14  
    15  	"camlistore.org/pkg/images"
    16  	"github.com/dustin/go-humanize"
    17  	"github.com/keybase/client/go/chat/attachments"
    18  	"github.com/keybase/client/go/chat/globals"
    19  	"github.com/keybase/client/go/chat/storage"
    20  	"github.com/keybase/client/go/chat/types"
    21  	"github.com/keybase/client/go/chat/utils"
    22  	"github.com/keybase/client/go/encrypteddb"
    23  	"github.com/keybase/client/go/libkb"
    24  	"github.com/keybase/client/go/protocol/chat1"
    25  	"github.com/keybase/client/go/protocol/gregor1"
    26  	"github.com/keybase/client/go/protocol/keybase1"
    27  )
    28  
    29  const (
    30  	minShortNameLength = 2
    31  	maxShortNameLength = 48
    32  	minEmojiSize       = 512        // min size for reading mime type
    33  	maxEmojiSize       = 256 * 1000 // 256kb
    34  	animationKey       = "emojianimations"
    35  )
    36  
    37  type EmojiValidationError struct {
    38  	Underlying error
    39  	CLIDisplay string
    40  	UIDisplay  string
    41  }
    42  
    43  func (e *EmojiValidationError) Error() string {
    44  	if e == nil || e.Underlying == nil {
    45  		return ""
    46  	}
    47  	return e.Underlying.Error()
    48  }
    49  
    50  func (e *EmojiValidationError) Export() *chat1.EmojiError {
    51  	if e == nil {
    52  		return nil
    53  	}
    54  	return &chat1.EmojiError{
    55  		Clidisplay: e.CLIDisplay,
    56  		Uidisplay:  e.UIDisplay,
    57  	}
    58  }
    59  
    60  func NewEmojiValidationError(err error, cliDisplay, uiDisplay string) *EmojiValidationError {
    61  	return &EmojiValidationError{
    62  		Underlying: err,
    63  		CLIDisplay: cliDisplay,
    64  		UIDisplay:  uiDisplay,
    65  	}
    66  }
    67  
    68  func NewEmojiValidationErrorSimple(err error, display string) *EmojiValidationError {
    69  	return &EmojiValidationError{
    70  		Underlying: err,
    71  		CLIDisplay: display,
    72  		UIDisplay:  display,
    73  	}
    74  }
    75  
    76  func NewEmojiValidationErrorJustError(err error) *EmojiValidationError {
    77  	return &EmojiValidationError{
    78  		Underlying: err,
    79  		CLIDisplay: err.Error(),
    80  		UIDisplay:  err.Error(),
    81  	}
    82  }
    83  
    84  type DevConvEmojiSource struct {
    85  	globals.Contextified
    86  	utils.DebugLabeler
    87  
    88  	aliasLookupLock sync.Mutex
    89  	aliasLookup     map[string]chat1.Emoji
    90  	ri              func() chat1.RemoteInterface
    91  	encryptedDB     *encrypteddb.EncryptedDB
    92  
    93  	// testing
    94  	tempDir                  string
    95  	testingCreatedSyncConv   chan struct{}
    96  	testingRefreshedSyncConv chan struct{}
    97  }
    98  
    99  var _ types.EmojiSource = (*DevConvEmojiSource)(nil)
   100  
   101  func NewDevConvEmojiSource(g *globals.Context, ri func() chat1.RemoteInterface) *DevConvEmojiSource {
   102  	keyFn := func(ctx context.Context) ([32]byte, error) {
   103  		return storage.GetSecretBoxKey(ctx, g.ExternalG())
   104  	}
   105  	dbFn := func(g *libkb.GlobalContext) *libkb.JSONLocalDb {
   106  		return g.LocalChatDb
   107  	}
   108  	return &DevConvEmojiSource{
   109  		Contextified: globals.NewContextified(g),
   110  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "DevConvEmojiSource", false),
   111  		ri:           ri,
   112  		encryptedDB:  encrypteddb.New(g.ExternalG(), dbFn, keyFn),
   113  	}
   114  }
   115  
   116  func (s *DevConvEmojiSource) makeStorage(topicType chat1.TopicType) types.ConvConversationBackedStorage {
   117  	return NewConvDevConversationBackedStorage(s.G(), topicType, false, s.ri)
   118  }
   119  
   120  func (s *DevConvEmojiSource) topicName(suffix *string) string {
   121  	ret := "emojis"
   122  	if suffix != nil {
   123  		ret += *suffix
   124  	}
   125  	return ret
   126  }
   127  
   128  func (s *DevConvEmojiSource) dbKey(uid gregor1.UID) libkb.DbKey {
   129  	return libkb.DbKey{
   130  		Typ: libkb.DBChatUserEmojis,
   131  		Key: uid.String(),
   132  	}
   133  }
   134  
   135  func (s *DevConvEmojiSource) getAliasLookup(ctx context.Context, uid gregor1.UID) (res map[string]chat1.Emoji, err error) {
   136  	s.aliasLookupLock.Lock()
   137  	defer s.aliasLookupLock.Unlock()
   138  	if s.aliasLookup != nil {
   139  		res = make(map[string]chat1.Emoji, len(s.aliasLookup))
   140  		for alias, emoji := range s.aliasLookup {
   141  			res[alias] = emoji
   142  		}
   143  		return res, nil
   144  	}
   145  	res = make(map[string]chat1.Emoji)
   146  	s.Debug(ctx, "getAliasLookup: missed alias lookup, reading from disk")
   147  	found, err := s.encryptedDB.Get(ctx, s.dbKey(uid), &res)
   148  	if err != nil {
   149  		return res, err
   150  	}
   151  	if !found {
   152  		return make(map[string]chat1.Emoji), nil
   153  	}
   154  	return res, nil
   155  }
   156  
   157  func (s *DevConvEmojiSource) putAliasLookup(ctx context.Context, uid gregor1.UID,
   158  	aliasLookup map[string]chat1.Emoji, opts chat1.EmojiFetchOpts) error {
   159  	s.aliasLookupLock.Lock()
   160  	defer s.aliasLookupLock.Unlock()
   161  	// set this if it is blank, or a full fetch
   162  	if !opts.OnlyInTeam || s.aliasLookup == nil {
   163  		s.aliasLookup = aliasLookup
   164  	}
   165  	// only commit to disk if this is a full lookup
   166  	if !opts.OnlyInTeam {
   167  		return s.encryptedDB.Put(ctx, s.dbKey(uid), s.aliasLookup)
   168  	}
   169  	return nil
   170  }
   171  
   172  func (s *DevConvEmojiSource) addAdvanced(ctx context.Context, uid gregor1.UID,
   173  	storageConv *chat1.ConversationLocal, convID chat1.ConversationID,
   174  	alias, filename string, allowOverwrite bool, storage types.ConvConversationBackedStorage) (res chat1.EmojiRemoteSource, err error) {
   175  	var stored chat1.EmojiStorage
   176  	alias = strings.ReplaceAll(alias, ":", "") // drop any colons from alias
   177  	if storageConv != nil {
   178  		_, err = storage.GetFromKnownConv(ctx, uid, *storageConv, &stored)
   179  	} else {
   180  		topicName := s.topicName(nil)
   181  		_, storageConv, err = storage.Get(ctx, uid, convID, topicName, &stored, true)
   182  	}
   183  	if err != nil {
   184  		return res, err
   185  	}
   186  	if stored.Mapping == nil {
   187  		stored.Mapping = make(map[string]chat1.EmojiRemoteSource)
   188  	}
   189  	if _, ok := stored.Mapping[alias]; ok && !allowOverwrite {
   190  		return res, NewEmojiValidationError(errors.New("alias already exists"),
   191  			"alias already exists, must specify --allow-overwrite to edit", "alias already exists")
   192  	}
   193  
   194  	sender := NewBlockingSender(s.G(), NewBoxer(s.G()), s.ri)
   195  	_, msgID, err := attachments.NewSender(s.G()).PostFileAttachment(ctx, sender, uid,
   196  		storageConv.GetConvID(), storageConv.Info.TlfName, keybase1.TLFVisibility_PRIVATE, nil, filename,
   197  		"", nil, 0, nil, nil)
   198  	if err != nil {
   199  		return res, err
   200  	}
   201  	if msgID == nil {
   202  		return res, errors.New("no messageID from attachment")
   203  	}
   204  	res = chat1.NewEmojiRemoteSourceWithMessage(chat1.EmojiMessage{
   205  		ConvID: storageConv.GetConvID(),
   206  		MsgID:  *msgID,
   207  	})
   208  	stored.Mapping[alias] = res
   209  	return res, storage.PutToKnownConv(ctx, uid, *storageConv, stored)
   210  }
   211  
   212  func (s *DevConvEmojiSource) IsStockEmoji(alias string) bool {
   213  	parts := strings.Split(alias, ":")
   214  	if len(parts) > 3 { // if we have a skin tone here, drop it
   215  		alias = fmt.Sprintf(":%s:", parts[1])
   216  	} else if len(parts) == 1 {
   217  		alias = fmt.Sprintf(":%s:", parts[0])
   218  	}
   219  	alias2 := strings.ReplaceAll(alias, "-", "_")
   220  	return storage.EmojiExists(alias) || storage.EmojiExists(alias2)
   221  }
   222  
   223  func (s *DevConvEmojiSource) normalizeShortName(shortName string) string {
   224  	return strings.ReplaceAll(shortName, ":", "") // drop any colons from alias
   225  }
   226  
   227  func (s *DevConvEmojiSource) validateShortName(shortName string) error {
   228  	if len(shortName) > maxShortNameLength {
   229  		err := errors.New("alias is too long")
   230  		return NewEmojiValidationError(err, fmt.Sprintf("alias is too long, must be less than %d",
   231  			maxShortNameLength), err.Error())
   232  	}
   233  	if len(shortName) < minShortNameLength {
   234  		err := errors.New("alias is too short")
   235  		return NewEmojiValidationError(err,
   236  			fmt.Sprintf("alias is too short, must be greater than %d", minShortNameLength),
   237  			err.Error())
   238  	}
   239  	if strings.Contains(shortName, "#") {
   240  		return NewEmojiValidationErrorJustError(errors.New("invalid character in alias"))
   241  	}
   242  	return nil
   243  }
   244  
   245  func (s *DevConvEmojiSource) validateCustomEmoji(ctx context.Context, shortName, filename string) (string, error) {
   246  	err := s.validateShortName(shortName)
   247  	if err != nil {
   248  		return "", err
   249  	}
   250  	shortName = s.normalizeShortName(shortName)
   251  
   252  	err = s.validateFile(ctx, filename)
   253  	if err != nil {
   254  		return "", err
   255  	}
   256  	return shortName, nil
   257  }
   258  
   259  // validateFile validates the following:
   260  // file size
   261  // format
   262  func (s *DevConvEmojiSource) validateFile(ctx context.Context, filename string) error {
   263  	finfo, err := attachments.StatOSOrKbfsFile(ctx, s.G().GlobalContext, filename)
   264  	if err != nil {
   265  		return NewEmojiValidationErrorSimple(err, "unable to open file")
   266  	}
   267  	if finfo.IsDir() {
   268  		return NewEmojiValidationErrorJustError(errors.New("unable to use a directory"))
   269  	} else if finfo.Size() > maxEmojiSize {
   270  		err := errors.New("file too large")
   271  		return NewEmojiValidationError(err,
   272  			fmt.Sprintf("emoji filesize too large, must be less than %s", humanize.Bytes(maxEmojiSize)),
   273  			err.Error())
   274  	} else if finfo.Size() < minEmojiSize {
   275  		err := errors.New("file too small")
   276  		return NewEmojiValidationError(err,
   277  			fmt.Sprintf("emoji filesize too small, must be greater than %s", humanize.Bytes(minEmojiSize)),
   278  			err.Error())
   279  	}
   280  
   281  	src, err := attachments.NewReadCloseResetter(ctx, s.G().GlobalContext, filename)
   282  	if err != nil {
   283  		return NewEmojiValidationErrorSimple(err, "failed to process file")
   284  	}
   285  	defer func() { src.Close() }()
   286  	if _, _, err = images.Decode(src, nil); err != nil {
   287  		s.Debug(ctx, "validateFile: failed to decode image: %s", err)
   288  		if err := src.Reset(); err != nil {
   289  			return NewEmojiValidationErrorSimple(err, "failed to process file")
   290  		}
   291  		g, err := gif.DecodeAll(src)
   292  		if err != nil {
   293  			s.Debug(ctx, "validateFile: failed to decode gif: %s", err)
   294  			return NewEmojiValidationErrorSimple(err, "invalid image file")
   295  		}
   296  		if len(g.Image) == 0 {
   297  			return NewEmojiValidationErrorJustError(errors.New("no image frames in GIF"))
   298  		}
   299  	}
   300  	return nil
   301  }
   302  
   303  func (s *DevConvEmojiSource) fromURL(ctx context.Context, url string) (string, error) {
   304  	resp, err := http.Get(url)
   305  	if err != nil {
   306  		return "", err
   307  	}
   308  	defer resp.Body.Close()
   309  	file, _, err := s.G().AttachmentUploader.GetUploadTempSink(ctx, "tmp-emoji")
   310  	if err != nil {
   311  		return "", err
   312  	}
   313  	_, err = io.Copy(file, resp.Body)
   314  	return file.Name(), err
   315  }
   316  
   317  func (s *DevConvEmojiSource) Add(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   318  	alias, filename string, allowOverwrite bool) (res chat1.EmojiRemoteSource, err error) {
   319  	defer s.Trace(ctx, &err, "Add")()
   320  	if strings.HasPrefix(filename, "http://") || strings.HasPrefix(filename, "https://") {
   321  		filename, err = s.fromURL(ctx, filename)
   322  		if err != nil {
   323  			return res, err
   324  		}
   325  		defer func() { _ = os.Remove(filename) }()
   326  	}
   327  	if alias, err = s.validateCustomEmoji(ctx, alias, filename); err != nil {
   328  		return res, err
   329  	}
   330  	storage := s.makeStorage(chat1.TopicType_EMOJI)
   331  	return s.addAdvanced(ctx, uid, nil, convID, alias, filename, allowOverwrite, storage)
   332  }
   333  
   334  func (s *DevConvEmojiSource) AddAlias(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   335  	newAlias, existingAlias string) (res chat1.EmojiRemoteSource, err error) {
   336  	defer s.Trace(ctx, &err, "AddAlias")()
   337  	if err = s.validateShortName(newAlias); err != nil {
   338  		return res, err
   339  	}
   340  	var stored chat1.EmojiStorage
   341  	storage := s.makeStorage(chat1.TopicType_EMOJI)
   342  	topicName := s.topicName(nil)
   343  	if _, _, err := storage.Get(ctx, uid, convID, topicName, &stored, false); err != nil {
   344  		return res, err
   345  	}
   346  	getExistingMsgSrc := func() (res chat1.EmojiRemoteSource, found bool) {
   347  		if stored.Mapping == nil {
   348  			return res, false
   349  		}
   350  		existingSource, ok := stored.Mapping[strings.Trim(existingAlias, ":")]
   351  		if !ok {
   352  			return res, false
   353  		}
   354  		if !existingSource.IsMessage() {
   355  			return res, false
   356  		}
   357  		return existingSource, true
   358  	}
   359  	msgSrc, ok := getExistingMsgSrc()
   360  	if ok {
   361  		res = chat1.NewEmojiRemoteSourceWithMessage(chat1.EmojiMessage{
   362  			ConvID:  msgSrc.Message().ConvID,
   363  			MsgID:   msgSrc.Message().MsgID,
   364  			IsAlias: true,
   365  		})
   366  		newAlias = s.normalizeShortName(newAlias)
   367  	} else if s.IsStockEmoji(existingAlias) {
   368  		username, err := s.G().GetUPAKLoader().LookupUsername(ctx, keybase1.UID(uid.String()))
   369  		if err != nil {
   370  			return res, err
   371  		}
   372  		res = chat1.NewEmojiRemoteSourceWithStockalias(chat1.EmojiStockAlias{
   373  			Text:     existingAlias,
   374  			Username: username.String(),
   375  			Time:     gregor1.ToTime(s.G().GetClock().Now()),
   376  		})
   377  	} else {
   378  		return res, fmt.Errorf("alias is not a valid existing custom emoji or stock emoji")
   379  	}
   380  	if stored.Mapping == nil {
   381  		stored.Mapping = make(map[string]chat1.EmojiRemoteSource)
   382  	}
   383  	stored.Mapping[newAlias] = res
   384  	return res, storage.Put(ctx, uid, convID, topicName, stored)
   385  }
   386  
   387  func (s *DevConvEmojiSource) removeRemoteSource(ctx context.Context, uid gregor1.UID,
   388  	conv chat1.ConversationLocal, source chat1.EmojiRemoteSource) error {
   389  	typ, err := source.Typ()
   390  	if err != nil {
   391  		return err
   392  	}
   393  	switch typ {
   394  	case chat1.EmojiRemoteSourceTyp_MESSAGE:
   395  		if source.Message().IsAlias {
   396  			s.Debug(ctx, "removeRemoteSource: skipping asset remove on alias")
   397  			return nil
   398  		}
   399  		return s.G().ChatHelper.DeleteMsg(ctx, source.Message().ConvID, conv.Info.TlfName,
   400  			source.Message().MsgID)
   401  	case chat1.EmojiRemoteSourceTyp_STOCKALIAS:
   402  		// do nothing
   403  	default:
   404  		return fmt.Errorf("unable to delete remote source of typ: %v", typ)
   405  	}
   406  	return nil
   407  }
   408  
   409  func (s *DevConvEmojiSource) Remove(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   410  	alias string) (err error) {
   411  	defer s.Trace(ctx, &err, "Remove")()
   412  	var stored chat1.EmojiStorage
   413  	storage := s.makeStorage(chat1.TopicType_EMOJI)
   414  	topicName := s.topicName(nil)
   415  	_, storageConv, err := storage.Get(ctx, uid, convID, topicName, &stored, true)
   416  	if err != nil {
   417  		return err
   418  	}
   419  	if storageConv == nil {
   420  		s.Debug(ctx, "Remove: no storage conv returned, bailing")
   421  		return nil
   422  	}
   423  	if stored.Mapping == nil {
   424  		s.Debug(ctx, "Remove: no mapping, bailing")
   425  		return nil
   426  	}
   427  	// get attachment message and delete it
   428  	source, ok := stored.Mapping[alias]
   429  	if !ok {
   430  		s.Debug(ctx, "Remove: no alias in mapping, bailing")
   431  		return nil
   432  	}
   433  	if err := s.removeRemoteSource(ctx, uid, *storageConv, source); err != nil {
   434  		s.Debug(ctx, "Remove: failed to remove remote source: %s", err)
   435  		return err
   436  	}
   437  	delete(stored.Mapping, alias)
   438  	// take out any aliases
   439  	if source.IsMessage() && !source.Message().IsAlias {
   440  		for existingAlias, existingSource := range stored.Mapping {
   441  			if existingSource.IsMessage() && existingSource.Message().IsAlias &&
   442  				existingSource.Message().MsgID == source.Message().MsgID {
   443  				delete(stored.Mapping, existingAlias)
   444  			}
   445  		}
   446  	}
   447  	return storage.Put(ctx, uid, convID, topicName, stored)
   448  }
   449  
   450  func (s *DevConvEmojiSource) animationsDisabled(ctx context.Context, uid gregor1.UID) bool {
   451  	st, err := s.G().GregorState.State(ctx)
   452  	if err != nil {
   453  		s.Debug(ctx, "animationsDisabled: failed to get state: %s", err)
   454  		return false
   455  	}
   456  	cat, err := gregor1.ObjFactory{}.MakeCategory(animationKey)
   457  	if err != nil {
   458  		s.Debug(ctx, "animationsDisabled: failed to make category: %s", err)
   459  		return false
   460  	}
   461  	items, err := st.ItemsInCategory(cat)
   462  	if err != nil {
   463  		s.Debug(ctx, "animationsDisabled: failed to get items: %s", err)
   464  		return false
   465  	}
   466  	return len(items) > 0
   467  }
   468  
   469  func (s *DevConvEmojiSource) ToggleAnimations(ctx context.Context, uid gregor1.UID, enabled bool) (err error) {
   470  	defer s.Trace(ctx, &err, "ToggleAnimations: enabled: %v", enabled)()
   471  	cat, err := gregor1.ObjFactory{}.MakeCategory(animationKey)
   472  	if err != nil {
   473  		s.Debug(ctx, "animationsDisabled: failed to make category: %s", err)
   474  		return err
   475  	}
   476  	if enabled {
   477  		return s.G().GregorState.DismissCategory(ctx, cat.(gregor1.Category))
   478  	}
   479  	_, err = s.G().GregorState.InjectItem(ctx, animationKey, []byte{1}, gregor1.TimeOrOffset{})
   480  	return err
   481  }
   482  
   483  func (s *DevConvEmojiSource) RemoteToLocalSource(ctx context.Context, uid gregor1.UID,
   484  	remote chat1.EmojiRemoteSource) (source chat1.EmojiLoadSource, noAnimSource chat1.EmojiLoadSource, err error) {
   485  	typ, err := remote.Typ()
   486  	if err != nil {
   487  		return source, noAnimSource, err
   488  	}
   489  	noAnim := s.animationsDisabled(ctx, uid)
   490  	switch typ {
   491  	case chat1.EmojiRemoteSourceTyp_MESSAGE:
   492  		msg := remote.Message()
   493  		sourceURL := s.G().AttachmentURLSrv.GetURL(ctx, msg.ConvID, msg.MsgID, false, noAnim, true)
   494  		noAnimSourceURL := s.G().AttachmentURLSrv.GetURL(ctx, msg.ConvID, msg.MsgID, false, true, true)
   495  		return chat1.NewEmojiLoadSourceWithHttpsrv(sourceURL),
   496  			chat1.NewEmojiLoadSourceWithHttpsrv(noAnimSourceURL), nil
   497  	case chat1.EmojiRemoteSourceTyp_STOCKALIAS:
   498  		ret := chat1.NewEmojiLoadSourceWithStr(remote.Stockalias().Text)
   499  		return ret, ret, nil
   500  	default:
   501  		return source, noAnimSource, errors.New("unknown remote source for local source")
   502  	}
   503  }
   504  
   505  func (s *DevConvEmojiSource) creationInfo(ctx context.Context, uid gregor1.UID,
   506  	remote chat1.EmojiRemoteSource) (res chat1.EmojiCreationInfo, err error) {
   507  	typ, err := remote.Typ()
   508  	if err != nil {
   509  		return res, err
   510  	}
   511  	reason := chat1.GetThreadReason_EMOJISOURCE
   512  	switch typ {
   513  	case chat1.EmojiRemoteSourceTyp_MESSAGE:
   514  		msg := remote.Message()
   515  		sourceMsg, err := s.G().ConvSource.GetMessage(ctx, msg.ConvID, uid, msg.MsgID, &reason, nil, false)
   516  		if err != nil {
   517  			return res, err
   518  		}
   519  		if !sourceMsg.IsValid() {
   520  			return res, errors.New("invalid message for creation info")
   521  		}
   522  		return chat1.EmojiCreationInfo{
   523  			Username: sourceMsg.Valid().SenderUsername,
   524  			Time:     sourceMsg.Valid().ServerHeader.Ctime,
   525  		}, nil
   526  	case chat1.EmojiRemoteSourceTyp_STOCKALIAS:
   527  		return chat1.EmojiCreationInfo{
   528  			Username: remote.Stockalias().Username,
   529  			Time:     remote.Stockalias().Time,
   530  		}, nil
   531  	default:
   532  		return res, errors.New("unknown remote source for creation info")
   533  	}
   534  }
   535  
   536  func (s *DevConvEmojiSource) getNoSet(ctx context.Context, uid gregor1.UID, convID *chat1.ConversationID,
   537  	opts chat1.EmojiFetchOpts) (res chat1.UserEmojis, aliasLookup map[string]chat1.Emoji, err error) {
   538  	aliasLookup = make(map[string]chat1.Emoji)
   539  	topicType := chat1.TopicType_EMOJI
   540  	storage := s.makeStorage(topicType)
   541  	var sourceTLFID *chat1.TLFID
   542  	if convID != nil {
   543  		conv, err := utils.GetUnverifiedConv(ctx, s.G(), uid, *convID, types.InboxSourceDataSourceAll)
   544  		if err != nil {
   545  			return res, aliasLookup, err
   546  		}
   547  		sourceTLFID = new(chat1.TLFID)
   548  		*sourceTLFID = conv.Conv.Metadata.IdTriple.Tlfid
   549  	}
   550  	readTopicName := s.topicName(nil)
   551  	ibox, _, err := s.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking,
   552  		types.InboxSourceDataSourceAll, nil, &chat1.GetInboxLocalQuery{
   553  			TopicType: &topicType,
   554  			TopicName: &readTopicName,
   555  		})
   556  	if err != nil {
   557  		return res, aliasLookup, err
   558  	}
   559  	convs := ibox.Convs
   560  	seenAliases := make(map[string]int)
   561  	addEmojis := func(convs []chat1.ConversationLocal, isCrossTeam bool) {
   562  		if opts.OnlyInTeam && isCrossTeam {
   563  			return
   564  		}
   565  		for _, conv := range convs {
   566  			var stored chat1.EmojiStorage
   567  			found, err := storage.GetFromKnownConv(ctx, uid, conv, &stored)
   568  			if err != nil {
   569  				s.Debug(ctx, "Get: failed to read from known conv: %s", err)
   570  				continue
   571  			}
   572  			if !found {
   573  				s.Debug(ctx, "Get: no stored info for: %s", conv.GetConvID())
   574  				continue
   575  			}
   576  			group := chat1.EmojiGroup{
   577  				Name: conv.Info.TlfName,
   578  			}
   579  			for alias, storedEmoji := range stored.Mapping {
   580  				if !opts.GetAliases && storedEmoji.IsAlias() {
   581  					continue
   582  				}
   583  				var creationInfo *chat1.EmojiCreationInfo
   584  				source, noAnimSource, err := s.RemoteToLocalSource(ctx, uid, storedEmoji)
   585  				if err != nil {
   586  					s.Debug(ctx, "Get: skipping emoji on remote-to-local error: %s", err)
   587  					continue
   588  				}
   589  				if opts.GetCreationInfo {
   590  					ci, err := s.creationInfo(ctx, uid, storedEmoji)
   591  					if err != nil {
   592  						s.Debug(ctx, "Get: failed to get creation info: %s", err)
   593  					} else {
   594  						creationInfo = new(chat1.EmojiCreationInfo)
   595  						*creationInfo = ci
   596  					}
   597  				}
   598  				teamname := conv.Info.TlfName
   599  				emoji := chat1.Emoji{
   600  					Alias:        alias,
   601  					Source:       source,
   602  					NoAnimSource: noAnimSource,
   603  					RemoteSource: storedEmoji,
   604  					IsCrossTeam:  isCrossTeam,
   605  					CreationInfo: creationInfo,
   606  					IsAlias:      storedEmoji.IsAlias(),
   607  					Teamname:     &teamname,
   608  				}
   609  				if seen, ok := seenAliases[alias]; ok {
   610  					seenAliases[alias]++
   611  					emoji.Alias += fmt.Sprintf("#%d", seen)
   612  				} else {
   613  					seenAliases[alias] = 2
   614  					if s.IsStockEmoji(alias) {
   615  						emoji.Alias += fmt.Sprintf("#%d", seenAliases[alias])
   616  						seenAliases[alias]++
   617  					}
   618  				}
   619  				aliasLookup[emoji.Alias] = emoji
   620  				group.Emojis = append(group.Emojis, emoji)
   621  			}
   622  			res.Emojis = append(res.Emojis, group)
   623  		}
   624  	}
   625  	var tlfConvs, otherConvs []chat1.ConversationLocal
   626  	for _, conv := range convs {
   627  		if sourceTLFID != nil && conv.Info.Triple.Tlfid.Eq(*sourceTLFID) {
   628  			tlfConvs = append(tlfConvs, conv)
   629  		} else {
   630  			otherConvs = append(otherConvs, conv)
   631  		}
   632  	}
   633  	addEmojis(tlfConvs, false)
   634  	addEmojis(otherConvs, sourceTLFID != nil)
   635  	return res, aliasLookup, nil
   636  }
   637  
   638  func (s *DevConvEmojiSource) Get(ctx context.Context, uid gregor1.UID, convID *chat1.ConversationID,
   639  	opts chat1.EmojiFetchOpts) (res chat1.UserEmojis, err error) {
   640  	defer s.Trace(ctx, &err, "Get %v", opts)()
   641  	var aliasLookup map[string]chat1.Emoji
   642  	if res, aliasLookup, err = s.getNoSet(ctx, uid, convID, opts); err != nil {
   643  		return res, err
   644  	}
   645  	if err := s.putAliasLookup(ctx, uid, aliasLookup, opts); err != nil {
   646  		s.Debug(ctx, "Get: failed to put alias lookup: %s", err)
   647  	}
   648  	for _, group := range res.Emojis {
   649  		sort.Slice(group.Emojis, func(i, j int) bool {
   650  			return group.Emojis[i].Alias < group.Emojis[j].Alias
   651  		})
   652  	}
   653  	return res, nil
   654  }
   655  
   656  type emojiMatch struct {
   657  	name     string
   658  	position []int
   659  }
   660  
   661  func (s *DevConvEmojiSource) parse(ctx context.Context, body string) (res []emojiMatch) {
   662  	body = utils.ReplaceQuotedSubstrings(body, false)
   663  	hits := globals.EmojiPattern.FindAllStringSubmatchIndex(body, -1)
   664  	for _, hit := range hits {
   665  		if len(hit) < 4 {
   666  			s.Debug(ctx, "parse: malformed hit: %d", len(hit))
   667  			continue
   668  		}
   669  		res = append(res, emojiMatch{
   670  			name:     body[hit[2]:hit[3]],
   671  			position: []int{hit[0], hit[1]},
   672  		})
   673  	}
   674  	return res
   675  }
   676  
   677  func (s *DevConvEmojiSource) stripAlias(alias string) string {
   678  	return strings.Split(alias, "#")[0]
   679  }
   680  
   681  func (s *DevConvEmojiSource) versionMatch(ctx context.Context, uid gregor1.UID, l chat1.EmojiRemoteSource,
   682  	r chat1.EmojiRemoteSource) bool {
   683  	if !l.IsMessage() || !r.IsMessage() {
   684  		return false
   685  	}
   686  	reason := chat1.GetThreadReason_EMOJISOURCE
   687  	lmsg, err := s.G().ConvSource.GetMessage(ctx, l.Message().ConvID, uid, l.Message().MsgID, &reason,
   688  		nil, false)
   689  	if err != nil {
   690  		s.Debug(ctx, "versionMatch: failed to get lmsg: %s", err)
   691  		return false
   692  	}
   693  	rmsg, err := s.G().ConvSource.GetMessage(ctx, r.Message().ConvID, uid, r.Message().MsgID, &reason,
   694  		nil, false)
   695  	if err != nil {
   696  		s.Debug(ctx, "versionMatch: failed to get rmsg: %s", err)
   697  		return false
   698  	}
   699  	if !lmsg.IsValid() || !rmsg.IsValid() {
   700  		s.Debug(ctx, "versionMatch: one message not valid: lmsg: %s rmsg: %s", lmsg.DebugString(),
   701  			rmsg.DebugString())
   702  		return false
   703  	}
   704  	if !lmsg.Valid().MessageBody.IsType(chat1.MessageType_ATTACHMENT) ||
   705  		!rmsg.Valid().MessageBody.IsType(chat1.MessageType_ATTACHMENT) {
   706  		s.Debug(ctx, "versionMatch: one message not attachment: lmsg: %s rmsg: %s", lmsg.DebugString(),
   707  			rmsg.DebugString())
   708  		return false
   709  	}
   710  	lhash := lmsg.Valid().MessageBody.Attachment().Object.PtHash
   711  	rhash := rmsg.Valid().MessageBody.Attachment().Object.PtHash
   712  	return lhash != nil && rhash != nil && lhash.Eq(rhash)
   713  }
   714  
   715  func (s *DevConvEmojiSource) getCrossTeamConv(ctx context.Context, uid gregor1.UID,
   716  	convID chat1.ConversationID, sourceConvID chat1.ConversationID) (res chat1.ConversationLocal, err error) {
   717  	baseConv, err := utils.GetVerifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
   718  	if err != nil {
   719  		s.Debug(ctx, "getCrossTeamConv: failed to get base conv: %s", err)
   720  		return res, err
   721  	}
   722  	sourceConv, err := utils.GetVerifiedConv(ctx, s.G(), uid, sourceConvID, types.InboxSourceDataSourceAll)
   723  	if err != nil {
   724  		s.Debug(ctx, "getCrossTeamConv: failed to get source conv: %s", err)
   725  		return res, err
   726  	}
   727  	var created bool
   728  	topicID := chat1.TopicID(sourceConv.Info.Triple.Tlfid.Bytes())
   729  	s.Debug(ctx, "getCrossTeamConv: attempting conv create: sourceConvID: %s topicID: %s",
   730  		sourceConv.GetConvID(), topicID)
   731  	topicName := topicID.String()
   732  	if res, created, err = NewConversation(ctx, s.G(), uid, baseConv.Info.TlfName, &topicName,
   733  		chat1.TopicType_EMOJICROSS, baseConv.GetMembersType(), baseConv.Info.Visibility,
   734  		&topicID, s.ri, NewConvFindExistingNormal); err != nil {
   735  		if convExistsErr, ok := err.(libkb.ChatConvExistsError); ok {
   736  			s.Debug(ctx, "getCrossTeamConv: conv exists error received, attempting to join: %s", err)
   737  			if err := JoinConversation(ctx, s.G(), s.DebugLabeler, s.ri, uid, convExistsErr.ConvID); err != nil {
   738  				s.Debug(ctx, "getCrossTeamConv: failed to join: %s", err)
   739  				return res, err
   740  			}
   741  			if res, err = utils.GetVerifiedConv(ctx, s.G(), uid, convExistsErr.ConvID,
   742  				types.InboxSourceDataSourceAll); err != nil {
   743  				s.Debug(ctx, "getCrossTeamConv: failed to get conv after successful join: %s", err)
   744  			}
   745  			created = false
   746  		} else {
   747  			return res, err
   748  		}
   749  	}
   750  	if created {
   751  		s.Debug(ctx, "getCrossTeamConv: created a new sync conv: %s (topicID: %s)", res.GetConvID(), topicID)
   752  		if s.testingCreatedSyncConv != nil {
   753  			s.testingCreatedSyncConv <- struct{}{}
   754  		}
   755  	} else {
   756  		s.Debug(ctx, "getCrossTeamConv: using exising sync conv: %s (topicID: %s)", res.GetConvID(), topicID)
   757  	}
   758  	return res, nil
   759  }
   760  
   761  func (s *DevConvEmojiSource) getCacheDir() string {
   762  	if len(s.tempDir) > 0 {
   763  		return s.tempDir
   764  	}
   765  	return s.G().GetCacheDir()
   766  }
   767  
   768  func (s *DevConvEmojiSource) syncCrossTeam(ctx context.Context, uid gregor1.UID, emoji chat1.HarvestedEmoji,
   769  	convID chat1.ConversationID) (res chat1.HarvestedEmoji, err error) {
   770  	typ, err := emoji.Source.Typ()
   771  	if err != nil {
   772  		return res, err
   773  	}
   774  	switch typ {
   775  	case chat1.EmojiRemoteSourceTyp_MESSAGE:
   776  	case chat1.EmojiRemoteSourceTyp_STOCKALIAS:
   777  		emoji.IsCrossTeam = true
   778  		return emoji, nil
   779  	default:
   780  		return res, errors.New("invalid remote source to sync")
   781  	}
   782  	var stored chat1.EmojiStorage
   783  	storage := s.makeStorage(chat1.TopicType_EMOJICROSS)
   784  	sourceConvID := emoji.Source.Message().ConvID
   785  	syncConv, err := s.getCrossTeamConv(ctx, uid, convID, sourceConvID)
   786  	if err != nil {
   787  		s.Debug(ctx, "syncCrossTeam: failed to get cross team conv: %s", err)
   788  		return res, err
   789  	}
   790  	if _, err := storage.GetFromKnownConv(ctx, uid, syncConv, &stored); err != nil {
   791  		s.Debug(ctx, "syncCrossTeam: failed to get from known conv: %s", err)
   792  		return res, err
   793  	}
   794  	if stored.Mapping == nil {
   795  		stored.Mapping = make(map[string]chat1.EmojiRemoteSource)
   796  	}
   797  
   798  	// check for a match
   799  	stripped := s.stripAlias(emoji.Alias)
   800  	if existing, ok := stored.Mapping[stripped]; ok {
   801  		s.Debug(ctx, "syncCrossTeam: hit mapping")
   802  		if s.versionMatch(ctx, uid, existing, emoji.Source) {
   803  			s.Debug(ctx, "syncCrossTeam: hit version, returning")
   804  			return chat1.HarvestedEmoji{
   805  				Alias:       emoji.Alias,
   806  				Source:      existing,
   807  				IsCrossTeam: true,
   808  			}, nil
   809  		}
   810  		s.Debug(ctx, "syncCrossTeam: missed on version")
   811  	} else {
   812  		s.Debug(ctx, "syncCrossTeam: missed mapping")
   813  	}
   814  	if s.testingRefreshedSyncConv != nil {
   815  		s.testingRefreshedSyncConv <- struct{}{}
   816  	}
   817  	// download from the original source
   818  	sink, err := os.CreateTemp(s.getCacheDir(), "emoji")
   819  	if err != nil {
   820  		return res, err
   821  	}
   822  	defer os.Remove(sink.Name())
   823  	if err := attachments.Download(ctx, s.G(), uid, sourceConvID,
   824  		emoji.Source.Message().MsgID, sink, false, nil, s.ri); err != nil {
   825  		s.Debug(ctx, "syncCrossTeam: failed to download: %s", err)
   826  		return res, err
   827  	}
   828  
   829  	// add the source to the target storage area
   830  	newSource, err := s.addAdvanced(ctx, uid, &syncConv, convID, stripped, sink.Name(), true, storage)
   831  	if err != nil {
   832  		return res, err
   833  	}
   834  	return chat1.HarvestedEmoji{
   835  		Alias:       emoji.Alias,
   836  		Source:      newSource,
   837  		IsCrossTeam: true,
   838  	}, nil
   839  }
   840  
   841  func (s *DevConvEmojiSource) Harvest(ctx context.Context, body string, uid gregor1.UID,
   842  	convID chat1.ConversationID, mode types.EmojiHarvestMode) (res []chat1.HarvestedEmoji, err error) {
   843  	if globals.IsEmojiHarvesterCtx(ctx) {
   844  		s.Debug(ctx, "Harvest: in an existing harvest context, bailing")
   845  		return nil, nil
   846  	}
   847  	matches := s.parse(ctx, body)
   848  	if len(matches) == 0 {
   849  		return nil, nil
   850  	}
   851  	ctx = globals.CtxMakeEmojiHarvester(ctx)
   852  	defer s.Trace(ctx, &err, "Harvest: mode: %v", mode)()
   853  	s.Debug(ctx, "Harvest: %d matches found", len(matches))
   854  	aliasMap, err := s.getAliasLookup(ctx, uid)
   855  	if err != nil {
   856  		s.Debug(ctx, "Harvest: failed to get alias lookup: %s", err)
   857  		return res, err
   858  	}
   859  	shouldSync := false
   860  	switch mode {
   861  	case types.EmojiHarvestModeNormal:
   862  		shouldSync = true
   863  		if len(aliasMap) == 0 {
   864  			s.Debug(ctx, "Harvest: no alias map, fetching fresh")
   865  			_, aliasMap, err = s.getNoSet(ctx, uid, &convID, chat1.EmojiFetchOpts{
   866  				GetCreationInfo: false,
   867  				GetAliases:      true,
   868  				OnlyInTeam:      false,
   869  			})
   870  			if err != nil {
   871  				s.Debug(ctx, "Harvest: failed to get emojis: %s", err)
   872  				return res, err
   873  			}
   874  		}
   875  	case types.EmojiHarvestModeFast:
   876  		// skip this, just use alias map in fast mode
   877  	}
   878  	if len(aliasMap) == 0 {
   879  		return nil, nil
   880  	}
   881  	s.Debug(ctx, "Harvest: num emojis: alias: %d", len(aliasMap))
   882  	for _, match := range matches {
   883  		// try group map first
   884  		if emoji, ok := aliasMap[match.name]; ok {
   885  			var resEmoji chat1.HarvestedEmoji
   886  			if emoji.IsCrossTeam && shouldSync {
   887  				if resEmoji, err = s.syncCrossTeam(ctx, uid, chat1.HarvestedEmoji{
   888  					Alias:  match.name,
   889  					Source: emoji.RemoteSource,
   890  				}, convID); err != nil {
   891  					s.Debug(ctx, "Harvest: failed to sync cross team: %s", err)
   892  					return res, err
   893  				}
   894  			} else {
   895  				resEmoji = chat1.HarvestedEmoji{
   896  					Alias:       match.name,
   897  					Source:      emoji.RemoteSource,
   898  					IsCrossTeam: emoji.IsCrossTeam,
   899  				}
   900  			}
   901  			res = append(res, resEmoji)
   902  		}
   903  	}
   904  	return res, nil
   905  }
   906  
   907  func (s *DevConvEmojiSource) Decorate(ctx context.Context, body string, uid gregor1.UID,
   908  	messageType chat1.MessageType, emojis []chat1.HarvestedEmoji) string {
   909  	if len(emojis) == 0 {
   910  		return body
   911  	}
   912  	matches := s.parse(ctx, body)
   913  	if len(matches) == 0 {
   914  		return body
   915  	}
   916  	bigEmoji := false
   917  	if messageType == chat1.MessageType_TEXT && len(matches) == 1 {
   918  		singleEmoji := matches[0]
   919  		// check if the emoji is the entire message (ignoring whitespace)
   920  		if singleEmoji.position[0] == 0 && singleEmoji.position[1] == len(strings.TrimSpace(body)) {
   921  			bigEmoji = true
   922  		}
   923  	}
   924  	defer s.Trace(ctx, nil, "Decorate")()
   925  	emojiMap := make(map[string]chat1.EmojiRemoteSource, len(emojis))
   926  	for _, emoji := range emojis {
   927  		// If we have conflicts on alias, use the first one. This helps make dealing with reactions
   928  		// better, since we really want the first reaction on an alias to always be displayed.
   929  		if _, ok := emojiMap[emoji.Alias]; !ok {
   930  			emojiMap[emoji.Alias] = emoji.Source
   931  		}
   932  	}
   933  	offset := 0
   934  	added := 0
   935  	isReacji := messageType == chat1.MessageType_REACTION
   936  	for _, match := range matches {
   937  		if remoteSource, ok := emojiMap[match.name]; ok {
   938  			source, noAnimSource, err := s.RemoteToLocalSource(ctx, uid, remoteSource)
   939  			if err != nil {
   940  				s.Debug(ctx, "Decorate: failed to get local source: %s", err)
   941  				continue
   942  			}
   943  			typ, err := source.Typ()
   944  			if err != nil {
   945  				s.Debug(ctx, "Decorate: failed to get load source type: %s", err)
   946  				continue
   947  			}
   948  			if typ == chat1.EmojiLoadSourceTyp_STR {
   949  				// Instead of decorating aliases, just replace them with the alias string
   950  				strDecoration := source.Str()
   951  				length := match.position[1] - match.position[0]
   952  				added := len(strDecoration) - length
   953  				decorationOffset := match.position[0] + offset
   954  				body = fmt.Sprintf("%s%s%s", body[:decorationOffset], strDecoration,
   955  					body[decorationOffset+length:])
   956  				offset += added
   957  				continue
   958  			}
   959  
   960  			body, added = utils.DecorateBody(ctx, body, match.position[0]+offset,
   961  				match.position[1]-match.position[0],
   962  				chat1.NewUITextDecorationWithEmoji(chat1.Emoji{
   963  					IsBig:        bigEmoji,
   964  					IsReacji:     isReacji,
   965  					Alias:        match.name,
   966  					Source:       source,
   967  					NoAnimSource: noAnimSource,
   968  					IsAlias:      remoteSource.IsAlias(),
   969  				}))
   970  			offset += added
   971  		}
   972  	}
   973  	return body
   974  }
   975  
   976  func (s *DevConvEmojiSource) IsValidSize(size int64) bool {
   977  	return size <= maxEmojiSize && size >= minEmojiSize
   978  }