github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/sync.go (about)

     1  package chat
     2  
     3  import (
     4  	"encoding/hex"
     5  	"sort"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/keybase/client/go/protocol/keybase1"
    10  
    11  	"github.com/keybase/client/go/chat/globals"
    12  	"github.com/keybase/client/go/chat/storage"
    13  	"github.com/keybase/client/go/chat/types"
    14  	"github.com/keybase/client/go/chat/utils"
    15  	"github.com/keybase/client/go/protocol/chat1"
    16  	"github.com/keybase/client/go/protocol/gregor1"
    17  	"github.com/keybase/clockwork"
    18  	"golang.org/x/net/context"
    19  )
    20  
    21  type Syncer struct {
    22  	globals.Contextified
    23  	utils.DebugLabeler
    24  	sync.Mutex
    25  
    26  	isConnected bool
    27  	offlinables []types.Offlinable
    28  
    29  	notificationLock    sync.Mutex
    30  	lastLoadedLock      sync.Mutex
    31  	clock               clockwork.Clock
    32  	sendDelay           time.Duration
    33  	shutdownCh          chan struct{}
    34  	fullReloadCh        chan gregor1.UID
    35  	flushCh             chan struct{}
    36  	notificationQueue   map[string][]chat1.ConversationStaleUpdate
    37  	fullReload          map[string]bool
    38  	lastLoadedConv      chat1.ConversationID
    39  	maxLimitedConvLoads int
    40  	maxConvLoads        int
    41  }
    42  
    43  func NewSyncer(g *globals.Context) *Syncer {
    44  	s := &Syncer{
    45  		Contextified:        globals.NewContextified(g),
    46  		DebugLabeler:        utils.NewDebugLabeler(g.ExternalG(), "Syncer", false),
    47  		isConnected:         false,
    48  		clock:               clockwork.NewRealClock(),
    49  		shutdownCh:          make(chan struct{}),
    50  		fullReloadCh:        make(chan gregor1.UID),
    51  		flushCh:             make(chan struct{}),
    52  		notificationQueue:   make(map[string][]chat1.ConversationStaleUpdate),
    53  		fullReload:          make(map[string]bool),
    54  		sendDelay:           time.Millisecond * 1000,
    55  		maxLimitedConvLoads: 3,
    56  		maxConvLoads:        10,
    57  	}
    58  	go s.sendNotificationLoop()
    59  	return s
    60  }
    61  
    62  func (s *Syncer) SetClock(clock clockwork.Clock) {
    63  	s.clock = clock
    64  }
    65  
    66  func (s *Syncer) Shutdown() {
    67  	s.Debug(context.Background(), "shutting down")
    68  	close(s.shutdownCh)
    69  }
    70  
    71  func (s *Syncer) dedupUpdates(updates []chat1.ConversationStaleUpdate) (res []chat1.ConversationStaleUpdate) {
    72  	m := make(map[chat1.ConvIDStr]chat1.ConversationStaleUpdate)
    73  	for _, update := range updates {
    74  		if existing, ok := m[update.ConvID.ConvIDStr()]; ok {
    75  			switch existing.UpdateType {
    76  			case chat1.StaleUpdateType_CLEAR:
    77  				// do nothing, existing is already clearing
    78  			case chat1.StaleUpdateType_NEWACTIVITY:
    79  				m[update.ConvID.ConvIDStr()] = update
    80  			}
    81  		} else {
    82  			m[update.ConvID.ConvIDStr()] = update
    83  		}
    84  	}
    85  	for _, update := range m {
    86  		res = append(res, update)
    87  	}
    88  	return res
    89  }
    90  
    91  func (s *Syncer) sendNotificationsOnce() {
    92  	s.notificationLock.Lock()
    93  	defer s.notificationLock.Unlock()
    94  
    95  	// Broadcast full reloads
    96  	for uid := range s.fullReload {
    97  		s.Debug(context.Background(), "flushing full reload: uid: %s", uid)
    98  		b, _ := hex.DecodeString(uid)
    99  		s.G().ActivityNotifier.InboxStale(context.Background(), gregor1.UID(b))
   100  	}
   101  	s.fullReload = make(map[string]bool)
   102  	// Broadcast conversation stales
   103  	for uid, updates := range s.notificationQueue {
   104  		updates = s.dedupUpdates(updates)
   105  		b, _ := hex.DecodeString(uid)
   106  		s.Debug(context.Background(), "flushing notifications: uid: %s len: %d", uid, len(updates))
   107  		for _, update := range updates {
   108  			s.Debug(context.Background(), "flushing: uid: %s convID: %s type: %v", uid,
   109  				update.ConvID, update.UpdateType)
   110  		}
   111  		s.G().ActivityNotifier.ThreadsStale(context.Background(), gregor1.UID(b), updates)
   112  	}
   113  	s.notificationQueue = make(map[string][]chat1.ConversationStaleUpdate)
   114  }
   115  
   116  func (s *Syncer) sendNotificationLoop() {
   117  	s.Debug(context.Background(), "starting notification loop")
   118  	for {
   119  		select {
   120  		case <-s.shutdownCh:
   121  			return
   122  		case uid := <-s.fullReloadCh:
   123  			s.notificationLock.Lock()
   124  			s.fullReload[uid.String()] = true
   125  			delete(s.notificationQueue, uid.String())
   126  			s.notificationLock.Unlock()
   127  			s.sendNotificationsOnce()
   128  		case <-s.clock.After(s.sendDelay):
   129  			s.sendNotificationsOnce()
   130  		case <-s.flushCh:
   131  			s.sendNotificationsOnce()
   132  		}
   133  	}
   134  }
   135  
   136  func (s *Syncer) SendChatStaleNotifications(ctx context.Context, uid gregor1.UID,
   137  	updates []chat1.ConversationStaleUpdate, immediate bool) {
   138  	if len(updates) == 0 {
   139  		s.Debug(ctx, "sending inbox stale message")
   140  		s.fullReloadCh <- uid
   141  	} else {
   142  		s.Debug(ctx, "sending thread stale messages: len: %d", len(updates))
   143  		for _, update := range updates {
   144  			s.Debug(ctx, "sending thread stale message: convID: %s type: %v", update.ConvID,
   145  				update.UpdateType)
   146  		}
   147  		s.notificationLock.Lock()
   148  		if !s.fullReload[uid.String()] {
   149  			s.notificationQueue[uid.String()] = append(s.notificationQueue[uid.String()], updates...)
   150  		}
   151  		s.notificationLock.Unlock()
   152  		if immediate {
   153  			s.flushCh <- struct{}{}
   154  		}
   155  	}
   156  }
   157  
   158  func (s *Syncer) isServerInboxClear(ctx context.Context, inbox *storage.Inbox, srvVers int) bool {
   159  	if _, err := s.G().ServerCacheVersions.MatchInbox(ctx, srvVers); err != nil {
   160  		s.Debug(ctx, "isServerInboxClear: inbox server version match error: %s", err.Error())
   161  		return true
   162  	}
   163  
   164  	return false
   165  }
   166  
   167  func (s *Syncer) IsConnected(ctx context.Context) bool {
   168  	s.Lock()
   169  	defer s.Unlock()
   170  	return s.isConnected
   171  }
   172  
   173  func (s *Syncer) Connected(ctx context.Context, cli chat1.RemoteInterface, uid gregor1.UID,
   174  	syncRes *chat1.SyncChatRes) (err error) {
   175  	ctx = globals.CtxAddLogTags(ctx, s.G())
   176  	defer s.Trace(ctx, &err, "Connected")()
   177  	s.Lock()
   178  	s.isConnected = true
   179  	// Let the Offlinables know that we are back online
   180  	for _, o := range s.offlinables {
   181  		o.Connected(ctx)
   182  	}
   183  	s.Unlock()
   184  
   185  	// Run sync against the server
   186  	return s.Sync(ctx, cli, uid, syncRes)
   187  }
   188  
   189  func (s *Syncer) Disconnected(ctx context.Context) {
   190  	defer s.Trace(ctx, nil, "Disconnected")()
   191  	s.Lock()
   192  	s.isConnected = false
   193  	// Let the Offlinables know of connection state change
   194  	for _, o := range s.offlinables {
   195  		o.Disconnected(ctx)
   196  	}
   197  	s.Unlock()
   198  }
   199  
   200  func (s *Syncer) handleMembersTypeChanged(ctx context.Context, uid gregor1.UID,
   201  	convIDs []chat1.ConversationID) {
   202  	// Clear caches from members type changed convos
   203  	for _, convID := range convIDs {
   204  		s.Debug(ctx, "handleMembersTypeChanged: clearing message cache: %s", convID)
   205  		err := s.G().ConvSource.Clear(ctx, convID, uid, nil)
   206  		if err != nil {
   207  			s.Debug(ctx, "handleMembersTypeChanged: erroring clearing conv: %+v", err)
   208  		}
   209  	}
   210  }
   211  
   212  func (s *Syncer) handleFilteredConvs(ctx context.Context, uid gregor1.UID, syncConvs []chat1.Conversation,
   213  	filteredConvs []types.RemoteConversation) {
   214  	fmap := make(map[chat1.ConvIDStr]bool)
   215  	for _, fconv := range filteredConvs {
   216  		fmap[fconv.Conv.GetConvID().ConvIDStr()] = true
   217  	}
   218  	// If any sync convs are not in the filtered list, let's blow away their local storage
   219  	for _, sconv := range syncConvs {
   220  		if !fmap[sconv.GetConvID().ConvIDStr()] {
   221  			s.Debug(ctx, "handleFilteredConvs: conv filtered from inbox, removing cache: convID: %s memberStatus: %v existence: %v",
   222  				sconv.GetConvID(), sconv.ReaderInfo.Status, sconv.Metadata.Existence)
   223  			err := s.G().ConvSource.Clear(ctx, sconv.GetConvID(), uid, nil)
   224  			if err != nil {
   225  				s.Debug(ctx, "handleFilteredCovs: erroring clearing conv: %+v", err)
   226  			}
   227  		}
   228  	}
   229  }
   230  
   231  func (s *Syncer) maxSyncUnboxConvs() int {
   232  	if s.G().IsMobileAppType() {
   233  		return 8
   234  	}
   235  	return 100
   236  }
   237  
   238  func (s *Syncer) getShouldUnboxSyncConvMap(ctx context.Context, convs []chat1.Conversation,
   239  	topicNameChanged []chat1.ConversationID) (m map[chat1.ConvIDStr]bool) {
   240  	m = make(map[chat1.ConvIDStr]bool)
   241  	for _, t := range topicNameChanged {
   242  		m[t.ConvIDStr()] = true
   243  	}
   244  	rconvs := utils.RemoteConvs(convs)
   245  	sort.Slice(rconvs, func(i, j int) bool {
   246  		return utils.GetConvPriorityScore(rconvs[i]) >= utils.GetConvPriorityScore(rconvs[j])
   247  	})
   248  	maxConvs := s.maxSyncUnboxConvs()
   249  	for _, conv := range rconvs {
   250  		if len(m) >= maxConvs {
   251  			s.Debug(ctx, "getShouldUnboxSyncConvMap: max sync convs reached, not including any others")
   252  			break
   253  		}
   254  		if m[conv.ConvIDStr] {
   255  			continue
   256  		}
   257  		if s.shouldUnboxSyncConv(conv.Conv) {
   258  			m[conv.ConvIDStr] = true
   259  		}
   260  	}
   261  	return m
   262  }
   263  
   264  func (s *Syncer) shouldUnboxSyncConv(conv chat1.Conversation) bool {
   265  	// only chat on mobile
   266  	if s.G().IsMobileAppType() && conv.GetTopicType() != chat1.TopicType_CHAT {
   267  		return false
   268  	}
   269  	// Skips convs we don't care for.
   270  	switch conv.Metadata.Status {
   271  	case chat1.ConversationStatus_BLOCKED,
   272  		chat1.ConversationStatus_IGNORED,
   273  		chat1.ConversationStatus_REPORTED:
   274  		return false
   275  	}
   276  	// Only let through ACTIVE/PREVIEW convs.
   277  	if conv.ReaderInfo != nil {
   278  		switch conv.ReaderInfo.Status {
   279  		case chat1.ConversationMemberStatus_ACTIVE,
   280  			chat1.ConversationMemberStatus_PREVIEW:
   281  		default:
   282  			return false
   283  		}
   284  	}
   285  	switch conv.GetMembersType() {
   286  	case chat1.ConversationMembersType_TEAM:
   287  		// include if this is a simple team or we are currently viewing the
   288  		// conv.
   289  		return conv.GetTopicType() == chat1.TopicType_KBFSFILEEDIT ||
   290  			conv.Metadata.TeamType != chat1.TeamType_COMPLEX ||
   291  			conv.GetConvID().Eq(s.GetSelectedConversation())
   292  	default:
   293  		return true
   294  	}
   295  }
   296  
   297  func (s *Syncer) notifyIncrementalSync(ctx context.Context, uid gregor1.UID,
   298  	allConvs []chat1.Conversation, shouldUnboxMap map[chat1.ConvIDStr]bool) {
   299  	if len(allConvs) == 0 {
   300  		s.Debug(ctx, "notifyIncrementalSync: no conversations given, sending a current result")
   301  		s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE,
   302  			chat1.NewChatSyncResultWithCurrent())
   303  		return
   304  	}
   305  	itemsByTopicType := make(map[chat1.TopicType][]chat1.ChatSyncIncrementalConv)
   306  	for _, c := range allConvs {
   307  		var md *types.RemoteConversationMetadata
   308  		rc, err := utils.GetUnverifiedConv(ctx, s.G(), uid, c.GetConvID(),
   309  			types.InboxSourceDataSourceLocalOnly)
   310  		if err == nil {
   311  			md = rc.LocalMetadata
   312  		}
   313  		rc = utils.RemoteConv(c)
   314  		rc.LocalMetadata = md
   315  		itemsByTopicType[c.GetTopicType()] = append(itemsByTopicType[c.GetTopicType()],
   316  			chat1.ChatSyncIncrementalConv{
   317  				Conv:        utils.PresentRemoteConversation(ctx, s.G(), uid, rc),
   318  				ShouldUnbox: shouldUnboxMap[c.GetConvID().ConvIDStr()],
   319  			})
   320  	}
   321  	for _, topicType := range chat1.TopicTypeMap {
   322  		if topicType == chat1.TopicType_NONE {
   323  			continue
   324  		}
   325  		s.G().ActivityNotifier.InboxSynced(ctx, uid, topicType,
   326  			chat1.NewChatSyncResultWithIncremental(chat1.ChatSyncIncrementalInfo{
   327  				Items: itemsByTopicType[topicType],
   328  			}))
   329  	}
   330  }
   331  
   332  func (s *Syncer) Sync(ctx context.Context, cli chat1.RemoteInterface, uid gregor1.UID,
   333  	syncRes *chat1.SyncChatRes) (err error) {
   334  	defer s.Trace(ctx, &err, "Sync")()
   335  	s.Lock()
   336  	if !s.isConnected {
   337  		defer s.Unlock()
   338  		s.Debug(ctx, "Sync: aborting because currently offline")
   339  		return OfflineError{}
   340  	}
   341  	s.Unlock()
   342  
   343  	// Grab current on disk version
   344  	ibox := storage.NewInbox(s.G())
   345  	vers, err := ibox.Version(ctx, uid)
   346  	if err != nil {
   347  		return err
   348  	}
   349  	srvVers, err := ibox.ServerVersion(ctx, uid)
   350  	if err != nil {
   351  		return err
   352  	}
   353  	s.Debug(ctx, "Sync: current inbox version: %v server version: %d", vers, srvVers)
   354  
   355  	if syncRes == nil {
   356  		// Run the sync call on the server to see how current our local copy is
   357  		syncRes = new(chat1.SyncChatRes)
   358  		if *syncRes, err = cli.SyncChat(ctx, chat1.SyncChatArg{
   359  			Vers:             vers,
   360  			SummarizeMaxMsgs: true,
   361  			ParticipantsMode: chat1.InboxParticipantsMode_SKIP_TEAMS,
   362  		}); err != nil {
   363  			s.Debug(ctx, "Sync: failed to sync inbox: %s", err.Error())
   364  			return err
   365  		}
   366  	} else {
   367  		s.Debug(ctx, "Sync: skipping sync call, data provided")
   368  	}
   369  
   370  	// Set new server versions
   371  	if err = s.G().ServerCacheVersions.Set(ctx, syncRes.CacheVers); err != nil {
   372  		s.Debug(ctx, "Sync: failed to set new server versions: %s", err.Error())
   373  	}
   374  
   375  	// Process what the server has told us to do with the local inbox copy
   376  	rtyp, err := syncRes.InboxRes.Typ()
   377  	if err != nil {
   378  		s.Debug(ctx, "Sync: strange type from SyncInbox: %s", err.Error())
   379  		return err
   380  	}
   381  	// Check if the server has cleared the inbox
   382  	if s.isServerInboxClear(ctx, ibox, srvVers) {
   383  		rtyp = chat1.SyncInboxResType_CLEAR
   384  	}
   385  
   386  	switch rtyp {
   387  	case chat1.SyncInboxResType_CLEAR:
   388  		s.Debug(ctx, "Sync: version out of date, clearing inbox: %v", vers)
   389  		if err = ibox.Clear(ctx, uid); err != nil {
   390  			s.Debug(ctx, "Sync: failed to clear inbox: %s", err.Error())
   391  		}
   392  		// Send notifications for a full clear
   393  		s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE,
   394  			chat1.NewChatSyncResultWithClear())
   395  	case chat1.SyncInboxResType_CURRENT:
   396  		s.Debug(ctx, "Sync: version is current, standing pat: %v", vers)
   397  		s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE,
   398  			chat1.NewChatSyncResultWithCurrent())
   399  	case chat1.SyncInboxResType_INCREMENTAL:
   400  		incr := syncRes.InboxRes.Incremental()
   401  		s.Debug(ctx, "Sync: version out of date, but can incrementally sync: old vers: %v vers: %v convs: %d",
   402  			vers, incr.Vers, len(incr.Convs))
   403  
   404  		var iboxSyncRes types.InboxSyncRes
   405  		expunges := make(map[chat1.ConvIDStr]chat1.Expunge)
   406  		if iboxSyncRes, err = s.G().InboxSource.Sync(ctx, uid, incr.Vers, incr.Convs); err != nil {
   407  			s.Debug(ctx, "Sync: failed to sync conversations to inbox: %s", err.Error())
   408  
   409  			// Send notifications for a full clear
   410  			s.G().ActivityNotifier.InboxSynced(ctx, uid, chat1.TopicType_NONE,
   411  				chat1.NewChatSyncResultWithClear())
   412  		} else {
   413  			s.handleMembersTypeChanged(ctx, uid, iboxSyncRes.MembersTypeChanged)
   414  			s.handleFilteredConvs(ctx, uid, incr.Convs, iboxSyncRes.FilteredConvs)
   415  			for _, expunge := range iboxSyncRes.Expunges {
   416  				expunges[expunge.ConvID.ConvIDStr()] = expunge.Expunge
   417  			}
   418  			// Send notifications for a successful partial sync
   419  			shouldUnboxMap := s.getShouldUnboxSyncConvMap(ctx, incr.Convs, iboxSyncRes.TopicNameChanged)
   420  			s.notifyIncrementalSync(ctx, uid, incr.Convs, shouldUnboxMap)
   421  		}
   422  
   423  		// The idea here is to limit the amount of work we do with the
   424  		// background conversation loader on mobile. If we are on a cell
   425  		// connection, or if we just came into the foreground, limit the number
   426  		// of conversations we queue up for background loading.
   427  		var queuedConvs, maxConvs int
   428  		pageBack := 3
   429  		num := 50
   430  		netState := s.G().MobileNetState.State()
   431  		state := s.G().MobileAppState.State()
   432  		if s.G().IsMobileAppType() {
   433  			maxConvs = s.maxConvLoads
   434  			num = 30
   435  			pageBack = 0
   436  			if netState.IsLimited() || state == keybase1.MobileAppState_FOREGROUND {
   437  				maxConvs = s.maxLimitedConvLoads
   438  			}
   439  		}
   440  		// Sort big teams convs lower (and by time to tie break)
   441  		sort.Slice(iboxSyncRes.FilteredConvs, func(i, j int) bool {
   442  			itype := iboxSyncRes.FilteredConvs[i].GetTeamType()
   443  			jtype := iboxSyncRes.FilteredConvs[j].GetTeamType()
   444  			if itype == chat1.TeamType_COMPLEX && jtype != chat1.TeamType_COMPLEX {
   445  				return false
   446  			}
   447  			if jtype == chat1.TeamType_COMPLEX && itype != chat1.TeamType_COMPLEX {
   448  				return true
   449  			}
   450  			return utils.GetConvPriorityScore(iboxSyncRes.FilteredConvs[i]) >= utils.GetConvPriorityScore(iboxSyncRes.FilteredConvs[j])
   451  		})
   452  
   453  		// Dispatch background jobs
   454  		for _, rc := range iboxSyncRes.FilteredConvs {
   455  			conv := rc.Conv
   456  			if expunge, ok := expunges[conv.GetConvID().ConvIDStr()]; ok {
   457  				// Run expunges on the background loader
   458  				s.Debug(ctx, "Sync: queueing expunge background loader job: convID: %s", conv.GetConvID())
   459  				job := types.NewConvLoaderJob(conv.GetConvID(), &chat1.Pagination{Num: num},
   460  					types.ConvLoaderPriorityHighest, types.ConvLoaderUnique,
   461  					func(ctx context.Context, tv chat1.ThreadView, job types.ConvLoaderJob) {
   462  						s.Debug(ctx, "Sync: executing expunge from a sync run: convID: %s", conv.GetConvID())
   463  						err := s.G().ConvSource.Expunge(ctx, conv, uid, expunge)
   464  						if err != nil {
   465  							s.Debug(ctx, "Sync: failed to expunge: %v", err)
   466  						}
   467  					})
   468  				if err := s.G().ConvLoader.Queue(ctx, job); err != nil {
   469  					s.Debug(ctx, "Sync: failed to queue conversation load: %s", err)
   470  				}
   471  				queuedConvs++
   472  			} else {
   473  				// If we set maxConvs, then check it now
   474  				if maxConvs > 0 && (queuedConvs >= maxConvs || !s.shouldUnboxSyncConv(conv)) {
   475  					continue
   476  				}
   477  				s.Debug(ctx, "Sync: queueing background loader job: convID: %s", conv.GetConvID())
   478  				// Everything else just queue up here
   479  				job := types.NewConvLoaderJob(conv.GetConvID(), &chat1.Pagination{Num: num},
   480  					types.ConvLoaderPriorityMedium, types.ConvLoaderGeneric,
   481  					newConvLoaderPagebackHook(s.G(), 0, pageBack))
   482  				if err := s.G().ConvLoader.Queue(ctx, job); err != nil {
   483  					s.Debug(ctx, "Sync: failed to queue conversation load: %s", err)
   484  				}
   485  				queuedConvs++
   486  			}
   487  		}
   488  	}
   489  
   490  	return nil
   491  }
   492  
   493  func (s *Syncer) RegisterOfflinable(offlinable types.Offlinable) {
   494  	s.Lock()
   495  	defer s.Unlock()
   496  	s.offlinables = append(s.offlinables, offlinable)
   497  }
   498  
   499  func (s *Syncer) GetSelectedConversation() chat1.ConversationID {
   500  	s.lastLoadedLock.Lock()
   501  	defer s.lastLoadedLock.Unlock()
   502  	return s.lastLoadedConv
   503  }
   504  
   505  func (s *Syncer) IsSelectedConversation(convID chat1.ConversationID) bool {
   506  	s.lastLoadedLock.Lock()
   507  	defer s.lastLoadedLock.Unlock()
   508  	return s.lastLoadedConv.Eq(convID)
   509  }
   510  
   511  func (s *Syncer) SelectConversation(ctx context.Context, convID chat1.ConversationID) {
   512  	s.lastLoadedLock.Lock()
   513  	defer s.lastLoadedLock.Unlock()
   514  	s.Debug(ctx, "SelectConversation: setting last loaded conv to: %s", convID)
   515  	s.lastLoadedConv = convID
   516  }