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

     1  package chat
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sort"
     7  	"time"
     8  
     9  	"github.com/keybase/client/go/chat/attachments"
    10  	"github.com/keybase/client/go/chat/globals"
    11  	"github.com/keybase/client/go/chat/storage"
    12  	"github.com/keybase/client/go/chat/types"
    13  	"github.com/keybase/client/go/chat/utils"
    14  	"github.com/keybase/client/go/libkb"
    15  	"github.com/keybase/client/go/protocol/chat1"
    16  	"github.com/keybase/client/go/protocol/gregor1"
    17  	"github.com/keybase/client/go/protocol/keybase1"
    18  	context "golang.org/x/net/context"
    19  )
    20  
    21  type baseConversationSource struct {
    22  	globals.Contextified
    23  	utils.DebugLabeler
    24  
    25  	boxer *Boxer
    26  	ri    func() chat1.RemoteInterface
    27  
    28  	blackoutPullForTesting bool
    29  }
    30  
    31  func newBaseConversationSource(g *globals.Context, ri func() chat1.RemoteInterface, boxer *Boxer) *baseConversationSource {
    32  	labeler := utils.NewDebugLabeler(g.ExternalG(), "baseConversationSource", false)
    33  	return &baseConversationSource{
    34  		Contextified: globals.NewContextified(g),
    35  		DebugLabeler: labeler,
    36  		ri:           ri,
    37  		boxer:        boxer,
    38  	}
    39  }
    40  
    41  func (s *baseConversationSource) SetRemoteInterface(ri func() chat1.RemoteInterface) {
    42  	s.ri = ri
    43  }
    44  
    45  // Sign implements github.com/keybase/go/chat/s3.Signer interface.
    46  func (s *baseConversationSource) Sign(payload []byte) ([]byte, error) {
    47  	arg := chat1.S3SignArg{
    48  		Payload:   payload,
    49  		Version:   1,
    50  		TempCreds: true,
    51  	}
    52  	return s.ri().S3Sign(context.Background(), arg)
    53  }
    54  
    55  // DeleteAssets implements github.com/keybase/go/chat/storage/storage.AssetDeleter interface.
    56  func (s *baseConversationSource) DeleteAssets(ctx context.Context, uid gregor1.UID,
    57  	convID chat1.ConversationID, assets []chat1.Asset) {
    58  	if len(assets) == 0 {
    59  		return
    60  	}
    61  	s.Debug(ctx, "DeleteAssets: deleting %d assets", len(assets))
    62  	// Fire off a background load of the thread with a post hook to delete the bodies cache
    63  	err := s.G().ConvLoader.Queue(ctx, types.NewConvLoaderJob(convID, &chat1.Pagination{Num: 0},
    64  		types.ConvLoaderPriorityHighest, types.ConvLoaderUnique,
    65  		func(ctx context.Context, tv chat1.ThreadView, job types.ConvLoaderJob) {
    66  			fetcher := s.G().AttachmentURLSrv.GetAttachmentFetcher()
    67  			if err := fetcher.DeleteAssets(ctx, convID, assets, s.ri, s); err != nil {
    68  				s.Debug(ctx, "DeleteAssets: Error purging ephemeral attachments %v", err)
    69  			}
    70  		}))
    71  	if err != nil {
    72  		s.Debug(ctx, "DeleteAssets: Error queuing conv job: %+v", err)
    73  	}
    74  }
    75  
    76  func (s *baseConversationSource) addPendingPreviews(ctx context.Context, thread *chat1.ThreadView) {
    77  	for index, m := range thread.Messages {
    78  		if !m.IsOutbox() {
    79  			continue
    80  		}
    81  		obr := m.Outbox()
    82  		if err := attachments.AddPendingPreview(ctx, s.G(), &obr); err != nil {
    83  			s.Debug(ctx, "addPendingPreviews: failed to get pending preview: outboxID: %s err: %s",
    84  				obr.OutboxID, err)
    85  			continue
    86  		}
    87  		thread.Messages[index] = chat1.NewMessageUnboxedWithOutbox(obr)
    88  	}
    89  }
    90  
    91  func (s *baseConversationSource) addConversationCards(ctx context.Context, uid gregor1.UID, reason chat1.GetThreadReason,
    92  	convID chat1.ConversationID, convOptional *chat1.ConversationLocal, thread *chat1.ThreadView) {
    93  	ctxShort, ctxShortCancel := context.WithTimeout(ctx, 2*time.Second)
    94  	defer ctxShortCancel()
    95  	if journeycardShouldNotRunOnReason[reason] {
    96  		s.Debug(ctx, "addConversationCards: skipping due to reason: %v", reason)
    97  		return
    98  	}
    99  	card, err := s.G().JourneyCardManager.PickCard(ctxShort, uid, convID, convOptional, thread)
   100  	ctxShortCancel()
   101  	if err != nil {
   102  		s.Debug(ctx, "addConversationCards: error getting next conversation card: %s", err)
   103  		return
   104  	}
   105  	if card == nil {
   106  		return
   107  	}
   108  	// Slot it in to the left of its prev.
   109  	addLeftOf := 0
   110  	for i := len(thread.Messages) - 1; i >= 0; i-- {
   111  		msgID := thread.Messages[i].GetMessageID()
   112  		if msgID != 0 && msgID >= card.PrevID {
   113  			addLeftOf = i
   114  			break
   115  		}
   116  	}
   117  	// Insert at index: https://github.com/golang/go/wiki/SliceTricks#insert
   118  	thread.Messages = append(thread.Messages, chat1.MessageUnboxed{})
   119  	copy(thread.Messages[addLeftOf+1:], thread.Messages[addLeftOf:])
   120  	thread.Messages[addLeftOf] = chat1.NewMessageUnboxedWithJourneycard(*card)
   121  }
   122  
   123  func (s *baseConversationSource) getRi(customRi func() chat1.RemoteInterface) chat1.RemoteInterface {
   124  	if customRi != nil {
   125  		return customRi()
   126  	}
   127  	return s.ri()
   128  }
   129  
   130  func (s *baseConversationSource) postProcessThread(ctx context.Context, uid gregor1.UID, reason chat1.GetThreadReason,
   131  	conv types.UnboxConversationInfo, thread *chat1.ThreadView, q *chat1.GetThreadQuery,
   132  	superXform types.SupersedesTransform, replyFiller types.ReplyFiller, checkPrev bool,
   133  	patchPagination bool, verifiedConv *chat1.ConversationLocal) (err error) {
   134  	if q != nil && q.DisablePostProcessThread {
   135  		return nil
   136  	}
   137  	s.Debug(ctx, "postProcessThread: thread messages starting out: %d", len(thread.Messages))
   138  	// Sanity check the prev pointers in this thread.
   139  	// TODO: We'll do this against what's in the cache once that's ready,
   140  	//       rather than only checking the messages we just fetched against
   141  	//       each other.
   142  
   143  	if s.blackoutPullForTesting {
   144  		thread.Messages = nil
   145  		return nil
   146  	}
   147  
   148  	if checkPrev {
   149  		_, _, err = CheckPrevPointersAndGetUnpreved(thread)
   150  		if err != nil {
   151  			return err
   152  		}
   153  	}
   154  
   155  	if patchPagination {
   156  		// Can mutate thread.Pagination.
   157  		s.patchPaginationLast(ctx, conv, uid, thread.Pagination, thread.Messages)
   158  	}
   159  
   160  	// Resolve supersedes & replies
   161  	deletedUpTo := conv.GetMaxDeletedUpTo()
   162  	if thread.Messages, err = s.TransformSupersedes(ctx, conv.GetConvID(), uid, thread.Messages, q, superXform,
   163  		replyFiller, &deletedUpTo); err != nil {
   164  		return err
   165  	}
   166  	s.Debug(ctx, "postProcessThread: thread messages after supersedes: %d", len(thread.Messages))
   167  
   168  	// Run type filter if it exists
   169  	thread.Messages = utils.FilterByType(thread.Messages, q, true)
   170  	s.Debug(ctx, "postProcessThread: thread messages after type filter: %d", len(thread.Messages))
   171  	// If we have exploded any messages while fetching them from cache, remove
   172  	// them now.
   173  	thread.Messages = utils.FilterExploded(conv, thread.Messages, s.boxer.clock.Now())
   174  	s.Debug(ctx, "postProcessThread: thread messages after explode filter: %d", len(thread.Messages))
   175  
   176  	// Add any conversation cards
   177  	s.addConversationCards(ctx, uid, reason, conv.GetConvID(), verifiedConv, thread)
   178  
   179  	// Fetch outbox and tack onto the result
   180  	outbox := storage.NewOutbox(s.G(), uid)
   181  	err = outbox.AppendToThread(ctx, conv.GetConvID(), thread)
   182  	switch err.(type) {
   183  	case nil, storage.MissError:
   184  	default:
   185  		return err
   186  	}
   187  	// Add attachment previews to pending messages
   188  	s.addPendingPreviews(ctx, thread)
   189  
   190  	return nil
   191  }
   192  
   193  func (s *baseConversationSource) TransformSupersedes(ctx context.Context,
   194  	convID chat1.ConversationID, uid gregor1.UID, msgs []chat1.MessageUnboxed,
   195  	q *chat1.GetThreadQuery, superXform types.SupersedesTransform, replyFiller types.ReplyFiller,
   196  	maxDeletedUpTo *chat1.MessageID) (res []chat1.MessageUnboxed, err error) {
   197  	defer s.Trace(ctx, &err, "TransformSupersedes")()
   198  	if q == nil || !q.DisableResolveSupersedes {
   199  		deletePlaceholders := q != nil && q.EnableDeletePlaceholders
   200  		if superXform == nil {
   201  			superXform = newBasicSupersedesTransform(s.G(), basicSupersedesTransformOpts{
   202  				UseDeletePlaceholders: deletePlaceholders,
   203  			})
   204  		}
   205  		if res, err = superXform.Run(ctx, convID, uid, msgs, maxDeletedUpTo); err != nil {
   206  			return nil, err
   207  		}
   208  	} else {
   209  		res = msgs
   210  	}
   211  	if replyFiller == nil {
   212  		replyFiller = NewReplyFiller(s.G())
   213  	}
   214  	return replyFiller.Fill(ctx, uid, convID, res)
   215  }
   216  
   217  // patchPaginationLast turns on page.Last if the messages are before InboxSource's view of Expunge.
   218  func (s *baseConversationSource) patchPaginationLast(ctx context.Context, conv types.UnboxConversationInfo, uid gregor1.UID,
   219  	page *chat1.Pagination, msgs []chat1.MessageUnboxed) {
   220  	if page == nil || page.Last {
   221  		return
   222  	}
   223  	if len(msgs) == 0 {
   224  		s.Debug(ctx, "patchPaginationLast: true - no msgs")
   225  		page.Last = true
   226  		return
   227  	}
   228  	expunge := conv.GetExpunge()
   229  	if expunge == nil {
   230  		s.Debug(ctx, "patchPaginationLast: no expunge info")
   231  		return
   232  	}
   233  	end1 := msgs[0].GetMessageID()
   234  	end2 := msgs[len(msgs)-1].GetMessageID()
   235  	if end1.Min(end2) <= expunge.Upto {
   236  		s.Debug(ctx, "patchPaginationLast: true - hit upto")
   237  		// If any message is prior to the nukepoint, say this is the last page.
   238  		page.Last = true
   239  	}
   240  }
   241  
   242  func (s *baseConversationSource) GetMessage(ctx context.Context, convID chat1.ConversationID,
   243  	uid gregor1.UID, msgID chat1.MessageID, reason *chat1.GetThreadReason, ri func() chat1.RemoteInterface,
   244  	resolveSupersedes bool) (chat1.MessageUnboxed, error) {
   245  	msgs, err := s.G().ConvSource.GetMessages(ctx, convID, uid, []chat1.MessageID{msgID},
   246  		reason, s.ri, resolveSupersedes)
   247  	if err != nil {
   248  		return chat1.MessageUnboxed{}, err
   249  	}
   250  	if len(msgs) != 1 {
   251  		return chat1.MessageUnboxed{}, errors.New("message not found")
   252  	}
   253  	return msgs[0], nil
   254  }
   255  
   256  func (s *baseConversationSource) PullFull(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID, reason chat1.GetThreadReason,
   257  	query *chat1.GetThreadQuery, maxPages *int) (res chat1.ThreadView, err error) {
   258  	ctx = libkb.WithLogTag(ctx, "PUL")
   259  	pagination := &chat1.Pagination{
   260  		Num: 300,
   261  	}
   262  	if maxPages == nil {
   263  		defaultMaxPages := 10000
   264  		maxPages = &defaultMaxPages
   265  	}
   266  	for i := 0; !pagination.Last && i < *maxPages; i++ {
   267  		thread, err := s.G().ConvSource.Pull(ctx, convID, uid, reason, nil, query, pagination)
   268  		if err != nil {
   269  			return res, err
   270  		}
   271  		res.Messages = append(res.Messages, thread.Messages...)
   272  		if thread.Pagination != nil {
   273  			pagination.Next = thread.Pagination.Next
   274  			pagination.Last = thread.Pagination.Last
   275  		}
   276  	}
   277  	return res, nil
   278  }
   279  
   280  func (s *baseConversationSource) getUnreadlineRemote(ctx context.Context, convID chat1.ConversationID,
   281  	readMsgID chat1.MessageID) (*chat1.MessageID, error) {
   282  	res, err := s.ri().GetUnreadlineRemote(ctx, chat1.GetUnreadlineRemoteArg{
   283  		ConvID:    convID,
   284  		ReadMsgID: readMsgID,
   285  	})
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	return res.UnreadlineID, nil
   290  }
   291  
   292  type RemoteConversationSource struct {
   293  	globals.Contextified
   294  	*baseConversationSource
   295  }
   296  
   297  var _ types.ConversationSource = (*RemoteConversationSource)(nil)
   298  
   299  func NewRemoteConversationSource(g *globals.Context, b *Boxer, ri func() chat1.RemoteInterface) *RemoteConversationSource {
   300  	return &RemoteConversationSource{
   301  		Contextified:           globals.NewContextified(g),
   302  		baseConversationSource: newBaseConversationSource(g, ri, b),
   303  	}
   304  }
   305  
   306  func (s *RemoteConversationSource) AcquireConversationLock(ctx context.Context, uid gregor1.UID,
   307  	convID chat1.ConversationID) error {
   308  	return nil
   309  }
   310  
   311  func (s *RemoteConversationSource) ReleaseConversationLock(ctx context.Context, uid gregor1.UID,
   312  	convID chat1.ConversationID) {
   313  }
   314  
   315  func (s *RemoteConversationSource) Push(ctx context.Context, convID chat1.ConversationID,
   316  	uid gregor1.UID, msg chat1.MessageBoxed) (chat1.MessageUnboxed, bool, error) {
   317  	// Do nothing here, we don't care about pushed messages
   318  
   319  	// The bool param here is to indicate the update given is continuous to our current state,
   320  	// which for this source is not relevant, so we just return true
   321  	return chat1.MessageUnboxed{}, true, nil
   322  }
   323  
   324  func (s *RemoteConversationSource) PushUnboxed(ctx context.Context, conv types.UnboxConversationInfo,
   325  	uid gregor1.UID, msg []chat1.MessageUnboxed) error {
   326  	return nil
   327  }
   328  
   329  func (s *RemoteConversationSource) Pull(ctx context.Context, convID chat1.ConversationID,
   330  	uid gregor1.UID, reason chat1.GetThreadReason, customRi func() chat1.RemoteInterface,
   331  	query *chat1.GetThreadQuery, pagination *chat1.Pagination) (chat1.ThreadView, error) {
   332  	ctx = libkb.WithLogTag(ctx, "PUL")
   333  
   334  	if convID.IsNil() {
   335  		return chat1.ThreadView{}, errors.New("RemoteConversationSource.Pull called with empty convID")
   336  	}
   337  
   338  	// Get conversation metadata
   339  	conv, err := utils.GetVerifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
   340  	if err != nil {
   341  		return chat1.ThreadView{}, err
   342  	}
   343  
   344  	// Fetch thread
   345  	rarg := chat1.GetThreadRemoteArg{
   346  		ConversationID: convID,
   347  		Query:          query,
   348  		Pagination:     pagination,
   349  		Reason:         reason,
   350  	}
   351  	boxed, err := s.getRi(customRi).GetThreadRemote(ctx, rarg)
   352  	if err != nil {
   353  		return chat1.ThreadView{}, err
   354  	}
   355  
   356  	thread, err := s.boxer.UnboxThread(ctx, boxed.Thread, conv)
   357  	if err != nil {
   358  		return chat1.ThreadView{}, err
   359  	}
   360  
   361  	// Post process thread before returning
   362  	if err = s.postProcessThread(ctx, uid, reason, conv, &thread, query, nil, nil, true, false, &conv); err != nil {
   363  		return chat1.ThreadView{}, err
   364  	}
   365  
   366  	return thread, nil
   367  }
   368  
   369  func (s *RemoteConversationSource) PullLocalOnly(ctx context.Context, convID chat1.ConversationID,
   370  	uid gregor1.UID, reason chat1.GetThreadReason, query *chat1.GetThreadQuery, pagination *chat1.Pagination, maxPlaceholders int) (chat1.ThreadView, error) {
   371  	return chat1.ThreadView{}, storage.MissError{Msg: "PullLocalOnly is unimplemented for RemoteConversationSource"}
   372  }
   373  
   374  func (s *RemoteConversationSource) Clear(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID, opts *types.ClearOpts) error {
   375  	return nil
   376  }
   377  
   378  func (s *RemoteConversationSource) GetMessages(ctx context.Context, convID chat1.ConversationID,
   379  	uid gregor1.UID, msgIDs []chat1.MessageID, threadReason *chat1.GetThreadReason,
   380  	customRi func() chat1.RemoteInterface, resolveSupersedes bool) (res []chat1.MessageUnboxed, err error) {
   381  	defer func() {
   382  		// unless arg says not to, transform the superseded messages
   383  		if !resolveSupersedes {
   384  			return
   385  		}
   386  		res, err = s.TransformSupersedes(ctx, convID, uid, res, nil, nil, nil, nil)
   387  	}()
   388  
   389  	rres, err := s.ri().GetMessagesRemote(ctx, chat1.GetMessagesRemoteArg{
   390  		ConversationID: convID,
   391  		MessageIDs:     msgIDs,
   392  		ThreadReason:   threadReason,
   393  	})
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  
   398  	conv := newBasicUnboxConversationInfo(convID, rres.MembersType, nil, rres.Visibility)
   399  	msgs, err := s.boxer.UnboxMessages(ctx, rres.Msgs, conv)
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  
   404  	return msgs, nil
   405  }
   406  
   407  func (s *RemoteConversationSource) GetMessagesWithRemotes(ctx context.Context,
   408  	conv chat1.Conversation, uid gregor1.UID, msgs []chat1.MessageBoxed) ([]chat1.MessageUnboxed, error) {
   409  	return s.boxer.UnboxMessages(ctx, msgs, conv)
   410  }
   411  
   412  func (s *RemoteConversationSource) GetUnreadline(ctx context.Context,
   413  	convID chat1.ConversationID, uid gregor1.UID, readMsgID chat1.MessageID) (*chat1.MessageID, error) {
   414  	return s.getUnreadlineRemote(ctx, convID, readMsgID)
   415  }
   416  
   417  func (s *RemoteConversationSource) Expunge(ctx context.Context,
   418  	conv types.UnboxConversationInfo, uid gregor1.UID, expunge chat1.Expunge) error {
   419  	return nil
   420  }
   421  
   422  func (s *RemoteConversationSource) EphemeralPurge(ctx context.Context, convID chat1.ConversationID,
   423  	uid gregor1.UID, purgeInfo *chat1.EphemeralPurgeInfo) (*chat1.EphemeralPurgeInfo, []chat1.MessageUnboxed, error) {
   424  	return nil, nil, nil
   425  }
   426  
   427  type HybridConversationSource struct {
   428  	globals.Contextified
   429  	utils.DebugLabeler
   430  	*baseConversationSource
   431  
   432  	numExpungeReload int
   433  	storage          *storage.Storage
   434  	lockTab          *utils.ConversationLockTab
   435  }
   436  
   437  var _ types.ConversationSource = (*HybridConversationSource)(nil)
   438  
   439  func NewHybridConversationSource(g *globals.Context, b *Boxer, storage *storage.Storage,
   440  	ri func() chat1.RemoteInterface) *HybridConversationSource {
   441  	return &HybridConversationSource{
   442  		Contextified:           globals.NewContextified(g),
   443  		DebugLabeler:           utils.NewDebugLabeler(g.ExternalG(), "HybridConversationSource", false),
   444  		baseConversationSource: newBaseConversationSource(g, ri, b),
   445  		storage:                storage,
   446  		lockTab:                utils.NewConversationLockTab(g),
   447  		numExpungeReload:       50,
   448  	}
   449  }
   450  
   451  func (s *HybridConversationSource) AcquireConversationLock(ctx context.Context, uid gregor1.UID,
   452  	convID chat1.ConversationID) error {
   453  	_, err := s.lockTab.Acquire(ctx, uid, convID)
   454  	return err
   455  }
   456  
   457  func (s *HybridConversationSource) ReleaseConversationLock(ctx context.Context, uid gregor1.UID,
   458  	convID chat1.ConversationID) {
   459  	s.lockTab.Release(ctx, uid, convID)
   460  }
   461  
   462  func (s *HybridConversationSource) isContinuousPush(ctx context.Context, convID chat1.ConversationID,
   463  	uid gregor1.UID, msgID chat1.MessageID) (continuousUpdate bool, err error) {
   464  	maxMsgID, err := s.storage.GetMaxMsgID(ctx, convID, uid)
   465  	switch err.(type) {
   466  	case storage.MissError:
   467  		continuousUpdate = true
   468  	case nil:
   469  		continuousUpdate = maxMsgID >= msgID-1
   470  	default:
   471  		return false, err
   472  	}
   473  	return continuousUpdate, nil
   474  }
   475  
   476  // completeAttachmentUpload removes any attachment previews from pending preview storage
   477  func (s *HybridConversationSource) completeAttachmentUpload(ctx context.Context, msg chat1.MessageUnboxed) {
   478  	if msg.GetMessageType() == chat1.MessageType_ATTACHMENT {
   479  		outboxID := msg.OutboxID()
   480  		if outboxID != nil {
   481  			s.G().AttachmentUploader.Complete(ctx, *outboxID)
   482  		}
   483  	}
   484  }
   485  
   486  func (s *HybridConversationSource) completeUnfurl(ctx context.Context, msg chat1.MessageUnboxed) {
   487  	if msg.GetMessageType() == chat1.MessageType_UNFURL {
   488  		outboxID := msg.OutboxID()
   489  		if outboxID != nil {
   490  			s.G().Unfurler.Complete(ctx, *outboxID)
   491  		}
   492  	}
   493  }
   494  
   495  func (s *HybridConversationSource) maybeNuke(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID, err *error) {
   496  	if err != nil && utils.IsDeletedConvError(*err) {
   497  		s.Debug(ctx, "purging caches on: %v for convID: %v, uid: %v", *err, convID, uid)
   498  		if ierr := s.Clear(ctx, convID, uid, &types.ClearOpts{
   499  			SendLocalAdminNotification: true,
   500  			Reason:                     "Got unexpected conversation deleted error. Cleared conv and inbox cache",
   501  		}); ierr != nil {
   502  			s.Debug(ctx, "unable to Clear conv: %v", ierr)
   503  		}
   504  		if ierr := s.G().InboxSource.Clear(ctx, uid, nil); ierr != nil {
   505  			s.Debug(ctx, "unable to Clear inbox: %v", ierr)
   506  		}
   507  		s.G().UIInboxLoader.UpdateLayout(ctx, chat1.InboxLayoutReselectMode_DEFAULT, "ConvSource#maybeNuke")
   508  		*err = nil
   509  	}
   510  }
   511  
   512  func (s *HybridConversationSource) Push(ctx context.Context, convID chat1.ConversationID,
   513  	uid gregor1.UID, msg chat1.MessageBoxed) (decmsg chat1.MessageUnboxed, continuousUpdate bool, err error) {
   514  	defer s.Trace(ctx, &err, "Push")()
   515  	if _, err = s.lockTab.Acquire(ctx, uid, convID); err != nil {
   516  		return decmsg, continuousUpdate, err
   517  	}
   518  	defer s.lockTab.Release(ctx, uid, convID)
   519  	defer s.maybeNuke(ctx, convID, uid, &err)
   520  
   521  	// Grab conversation information before pushing
   522  	conv, err := utils.GetUnverifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
   523  	if err != nil {
   524  		return decmsg, continuousUpdate, err
   525  	}
   526  
   527  	// Check to see if we are "appending" this message to the current record.
   528  	if continuousUpdate, err = s.isContinuousPush(ctx, convID, uid, msg.GetMessageID()); err != nil {
   529  		return decmsg, continuousUpdate, err
   530  	}
   531  
   532  	decmsg, err = s.boxer.UnboxMessage(ctx, msg, conv, nil)
   533  	if err != nil {
   534  		return decmsg, continuousUpdate, err
   535  	}
   536  
   537  	// Check conversation ID and change to error if it is wrong
   538  	if decmsg.IsValid() && !decmsg.Valid().ClientHeader.Conv.Derivable(convID) {
   539  		s.Debug(ctx, "invalid conversation ID detected, not derivable: %s", convID)
   540  		decmsg = chat1.NewMessageUnboxedWithError(chat1.MessageUnboxedError{
   541  			ErrMsg:      "invalid conversation ID",
   542  			MessageID:   msg.GetMessageID(),
   543  			MessageType: msg.GetMessageType(),
   544  		})
   545  	}
   546  
   547  	// Add to the local storage
   548  	if err = s.mergeMaybeNotify(ctx, conv, uid, []chat1.MessageUnboxed{decmsg}, chat1.GetThreadReason_GENERAL); err != nil {
   549  		return decmsg, continuousUpdate, err
   550  	}
   551  	if msg.ClientHeader.Sender.Eq(uid) && conv.GetMembersType() == chat1.ConversationMembersType_TEAM {
   552  		teamID, err := keybase1.TeamIDFromString(conv.Conv.Metadata.IdTriple.Tlfid.String())
   553  		if err != nil {
   554  			s.Debug(ctx, "Push: failed to get team ID: %v", err)
   555  		} else {
   556  			go s.G().JourneyCardManager.SentMessage(globals.BackgroundChatCtx(ctx, s.G()), uid, teamID, convID)
   557  		}
   558  	}
   559  	// Remove any pending previews from storage
   560  	s.completeAttachmentUpload(ctx, decmsg)
   561  	// complete any active unfurl
   562  	s.completeUnfurl(ctx, decmsg)
   563  
   564  	return decmsg, continuousUpdate, nil
   565  }
   566  
   567  func (s *HybridConversationSource) PushUnboxed(ctx context.Context, conv types.UnboxConversationInfo,
   568  	uid gregor1.UID, msgs []chat1.MessageUnboxed) (err error) {
   569  	defer s.Trace(ctx, &err, "PushUnboxed")()
   570  	convID := conv.GetConvID()
   571  	if _, err = s.lockTab.Acquire(ctx, uid, convID); err != nil {
   572  		return err
   573  	}
   574  	defer s.lockTab.Release(ctx, uid, convID)
   575  	defer s.maybeNuke(ctx, convID, uid, &err)
   576  
   577  	// sanity check against conv ID
   578  	for _, msg := range msgs {
   579  		if msg.IsValid() && !msg.Valid().ClientHeader.Conv.Derivable(convID) {
   580  			s.Debug(ctx, "PushUnboxed: pushing an unboxed message from wrong conv: correct: %s trip: %+v id: %d",
   581  				convID, msg.Valid().ClientHeader.Conv, msg.GetMessageID())
   582  			return errors.New("cannot push into a different conversation")
   583  		}
   584  	}
   585  	if err = s.mergeMaybeNotify(ctx, conv, uid, msgs, chat1.GetThreadReason_PUSH); err != nil {
   586  		return err
   587  	}
   588  	return nil
   589  }
   590  
   591  func (s *HybridConversationSource) resolveHoles(ctx context.Context, uid gregor1.UID,
   592  	thread *chat1.ThreadView, conv chat1.Conversation, reason chat1.GetThreadReason,
   593  	customRi func() chat1.RemoteInterface) (err error) {
   594  	defer s.Trace(ctx, &err, "resolveHoles")()
   595  	var msgIDs []chat1.MessageID
   596  	// Gather all placeholder messages so we can go fetch them
   597  	for index, msg := range thread.Messages {
   598  		if msg.IsPlaceholder() {
   599  			if index == len(thread.Messages)-1 {
   600  				// If the last message is a hole, we might not have fetched everything,
   601  				// so fail this case like a normal miss
   602  				return storage.MissError{}
   603  			}
   604  			msgIDs = append(msgIDs, msg.GetMessageID())
   605  		}
   606  	}
   607  	if len(msgIDs) == 0 {
   608  		// Nothing to do
   609  		return nil
   610  	}
   611  	// Fetch all missing messages from server, and sub in the real ones into the placeholder slots
   612  	msgs, err := s.GetMessages(ctx, conv.GetConvID(), uid, msgIDs, &reason, customRi, false)
   613  	if err != nil {
   614  		s.Debug(ctx, "resolveHoles: failed to get missing messages: %s", err.Error())
   615  		return err
   616  	}
   617  	s.Debug(ctx, "resolveHoles: success: filled %d holes", len(msgs))
   618  	msgMap := make(map[chat1.MessageID]chat1.MessageUnboxed)
   619  	for _, msg := range msgs {
   620  		msgMap[msg.GetMessageID()] = msg
   621  	}
   622  	for index, msg := range thread.Messages {
   623  		if msg.IsPlaceholder() {
   624  			newMsg, ok := msgMap[msg.GetMessageID()]
   625  			if !ok {
   626  				return fmt.Errorf("failed to find hole resolution: %v", msg.GetMessageID())
   627  			}
   628  			thread.Messages[index] = newMsg
   629  		}
   630  	}
   631  	return nil
   632  }
   633  
   634  func (s *HybridConversationSource) getConvForPull(ctx context.Context, uid gregor1.UID,
   635  	convID chat1.ConversationID) (res types.RemoteConversation, err error) {
   636  	rconv, err := utils.GetUnverifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
   637  	if err != nil {
   638  		return res, err
   639  	}
   640  	if !rconv.Conv.HasMemberStatus(chat1.ConversationMemberStatus_NEVER_JOINED) {
   641  		return rconv, nil
   642  	}
   643  	s.Debug(ctx, "getConvForPull: in conversation with never joined, getting conv from remote")
   644  	return utils.GetUnverifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceRemoteOnly)
   645  }
   646  
   647  // maxHolesForPull is the number of misses in the body storage cache we will tolerate missing. A good
   648  // way to think about this number is the number of extra reads from the cache we need to do before
   649  // formally declaring the request a failure.
   650  var maxHolesForPull = 50
   651  
   652  func (s *HybridConversationSource) Pull(ctx context.Context, convID chat1.ConversationID,
   653  	uid gregor1.UID, reason chat1.GetThreadReason, customRi func() chat1.RemoteInterface,
   654  	query *chat1.GetThreadQuery, pagination *chat1.Pagination) (thread chat1.ThreadView, err error) {
   655  	ctx = libkb.WithLogTag(ctx, "PUL")
   656  	defer s.Trace(ctx, &err, "Pull(%s)", convID)()
   657  	if convID.IsNil() {
   658  		return chat1.ThreadView{}, errors.New("HybridConversationSource.Pull called with empty convID")
   659  	}
   660  	if _, err = s.lockTab.Acquire(ctx, uid, convID); err != nil {
   661  		return thread, err
   662  	}
   663  	defer s.lockTab.Release(ctx, uid, convID)
   664  	defer s.maybeNuke(ctx, convID, uid, &err)
   665  
   666  	// Get conversation metadata
   667  	rconv, err := s.getConvForPull(ctx, uid, convID)
   668  	var unboxConv types.UnboxConversationInfo
   669  	if err == nil && !rconv.Conv.HasMemberStatus(chat1.ConversationMemberStatus_NEVER_JOINED) {
   670  		conv := rconv.Conv
   671  		unboxConv = conv
   672  		// Try locally first
   673  		rc := storage.NewHoleyResultCollector(maxHolesForPull,
   674  			s.storage.ResultCollectorFromQuery(ctx, query, pagination))
   675  		thread, err = s.fetchMaybeNotify(ctx, conv.GetConvID(), uid, rc, conv.ReaderInfo.MaxMsgid,
   676  			query, pagination)
   677  		if err == nil {
   678  			// Since we are using the "holey" collector, we need to resolve any placeholder
   679  			// messages that may have been fetched.
   680  			s.Debug(ctx, "Pull: (holey) cache hit: convID: %s uid: %s holes: %d msgs: %d",
   681  				unboxConv.GetConvID(), uid, rc.Holes(), len(thread.Messages))
   682  			err = s.resolveHoles(ctx, uid, &thread, conv, reason, customRi)
   683  		}
   684  		if err == nil {
   685  			// Before returning the stuff, send remote request to mark as read if
   686  			// requested.
   687  			if query != nil && query.MarkAsRead && len(thread.Messages) > 0 {
   688  				readMsgID := thread.Messages[0].GetMessageID()
   689  				if err = s.G().InboxSource.MarkAsRead(ctx, convID, uid, &readMsgID, false /* forceUnread */); err != nil {
   690  					return chat1.ThreadView{}, err
   691  				}
   692  				if _, err = s.G().InboxSource.ReadMessage(ctx, uid, 0, convID, readMsgID); err != nil {
   693  					return chat1.ThreadView{}, err
   694  				}
   695  			} else {
   696  				s.Debug(ctx, "Pull: skipping mark as read call")
   697  			}
   698  			// Run post process stuff
   699  			vconv, err := utils.GetVerifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
   700  			if err == nil {
   701  				if err = s.postProcessThread(ctx, uid, reason, conv, &thread, query, nil, nil, true, true, &vconv); err != nil {
   702  					return thread, err
   703  				}
   704  			}
   705  			return thread, nil
   706  		}
   707  		s.Debug(ctx, "Pull: cache miss: err: %s", err)
   708  	} else {
   709  		s.Debug(ctx, "Pull: error fetching conv metadata: convID: %s uid: %s err: %s", convID, uid, err)
   710  	}
   711  
   712  	// Fetch the entire request on failure
   713  	rarg := chat1.GetThreadRemoteArg{
   714  		ConversationID: convID,
   715  		Query:          query,
   716  		Pagination:     pagination,
   717  		Reason:         reason,
   718  	}
   719  	boxed, err := s.getRi(customRi).GetThreadRemote(ctx, rarg)
   720  	if err != nil {
   721  		return chat1.ThreadView{}, err
   722  	}
   723  	s.Debug(ctx, "Pull: pagination req: %+v, pagination resp: %+v", pagination, boxed.Thread.Pagination)
   724  
   725  	// Set up public inbox info if we don't have one with members type from remote call. Assume this is a
   726  	// public chat here, since it is the only chance we have to unbox it.
   727  	if unboxConv == nil {
   728  		unboxConv = newExtraInboxUnboxConverstionInfo(convID, boxed.MembersType, boxed.Visibility)
   729  	}
   730  
   731  	// Unbox
   732  	thread, err = s.boxer.UnboxThread(ctx, boxed.Thread, unboxConv)
   733  	if err != nil {
   734  		return chat1.ThreadView{}, err
   735  	}
   736  
   737  	// Store locally (just warn on error, don't abort the whole thing)
   738  	if err = s.mergeMaybeNotify(ctx, unboxConv, uid, thread.Messages, reason); err != nil {
   739  		s.Debug(ctx, "Pull: unable to commit thread locally: convID: %s uid: %s", convID, uid)
   740  	}
   741  
   742  	// Run post process stuff
   743  	if err = s.postProcessThread(ctx, uid, reason, unboxConv, &thread, query, nil, nil, true, true, nil); err != nil {
   744  		return thread, err
   745  	}
   746  	return thread, nil
   747  }
   748  
   749  type pullLocalResultCollector struct {
   750  	storage.ResultCollector
   751  }
   752  
   753  func (p *pullLocalResultCollector) Name() string {
   754  	return "pulllocal"
   755  }
   756  
   757  func (p *pullLocalResultCollector) String() string {
   758  	return fmt.Sprintf("[ %s: base: %s ]", p.Name(), p.ResultCollector)
   759  }
   760  
   761  func (p *pullLocalResultCollector) hasRealResults() bool {
   762  	for _, m := range p.Result() {
   763  		st, err := m.State()
   764  		if err != nil {
   765  			// count these
   766  			return true
   767  		}
   768  		switch st {
   769  		case chat1.MessageUnboxedState_PLACEHOLDER:
   770  			// don't count!
   771  		default:
   772  			return true
   773  		}
   774  	}
   775  	return false
   776  }
   777  
   778  func (p *pullLocalResultCollector) Error(err storage.Error) storage.Error {
   779  	// Swallow this error, we know we can miss if we get anything at all
   780  	if _, ok := err.(storage.MissError); ok && p.hasRealResults() {
   781  		return nil
   782  	}
   783  	return err
   784  }
   785  
   786  func newPullLocalResultCollector(baseRC storage.ResultCollector) *pullLocalResultCollector {
   787  	return &pullLocalResultCollector{
   788  		ResultCollector: baseRC,
   789  	}
   790  }
   791  
   792  func (s *HybridConversationSource) PullLocalOnly(ctx context.Context, convID chat1.ConversationID,
   793  	uid gregor1.UID, reason chat1.GetThreadReason, query *chat1.GetThreadQuery, pagination *chat1.Pagination, maxPlaceholders int) (tv chat1.ThreadView, err error) {
   794  	ctx = libkb.WithLogTag(ctx, "PUL")
   795  	defer s.Trace(ctx, &err, "PullLocalOnly")()
   796  	if _, err = s.lockTab.Acquire(ctx, uid, convID); err != nil {
   797  		return tv, err
   798  	}
   799  	defer s.lockTab.Release(ctx, uid, convID)
   800  	defer s.maybeNuke(ctx, convID, uid, &err)
   801  
   802  	// Post process thread before returning
   803  	defer func() {
   804  		if err == nil {
   805  			superXform := newBasicSupersedesTransform(s.G(), basicSupersedesTransformOpts{})
   806  			superXform.SetMessagesFunc(func(ctx context.Context, convID chat1.ConversationID,
   807  				uid gregor1.UID, msgIDs []chat1.MessageID,
   808  				_ *chat1.GetThreadReason, _ func() chat1.RemoteInterface, _ bool) (res []chat1.MessageUnboxed, err error) {
   809  				msgs, err := storage.New(s.G(), s).FetchMessages(ctx, convID, uid, msgIDs)
   810  				if err != nil {
   811  					return nil, err
   812  				}
   813  				for _, msg := range msgs {
   814  					if msg != nil {
   815  						res = append(res, *msg)
   816  					}
   817  				}
   818  				return res, nil
   819  			})
   820  			replyFiller := NewReplyFiller(s.G(), LocalOnlyReplyFill(true))
   821  			// Form a fake version of a conversation so we don't need to hit the network ever here
   822  			var conv chat1.Conversation
   823  			conv.Metadata.ConversationID = convID
   824  			err = s.postProcessThread(ctx, uid, reason, conv, &tv, query, superXform, replyFiller, false,
   825  				true, nil)
   826  		}
   827  	}()
   828  
   829  	// Fetch the inbox max message ID as well to compare against the local stored max messages
   830  	// if the caller is ok with receiving placeholders
   831  	var iboxMaxMsgID chat1.MessageID
   832  	if maxPlaceholders > 0 {
   833  		iboxRes, err := storage.NewInbox(s.G()).GetConversation(ctx, uid, convID)
   834  		if err != nil {
   835  			s.Debug(ctx, "PullLocalOnly: failed to read inbox for conv, not using: %s", err)
   836  		} else if iboxRes.Conv.ReaderInfo == nil {
   837  			s.Debug(ctx, "PullLocalOnly: no reader infoconv returned for conv, not using")
   838  		} else {
   839  			iboxMaxMsgID = iboxRes.Conv.ReaderInfo.MaxMsgid
   840  			s.Debug(ctx, "PullLocalOnly: found ibox max msgid: %d", iboxMaxMsgID)
   841  		}
   842  	}
   843  
   844  	// A number < 0 means it will fetch until it hits the end of the local copy. Our special
   845  	// result collector will suppress any miss errors
   846  	num := -1
   847  	if pagination != nil {
   848  		num = pagination.Num
   849  	}
   850  	baseRC := s.storage.ResultCollectorFromQuery(ctx, query, pagination)
   851  	baseRC.SetTarget(num)
   852  	rc := storage.NewHoleyResultCollector(maxPlaceholders, newPullLocalResultCollector(baseRC))
   853  	tv, err = s.fetchMaybeNotify(ctx, convID, uid, rc, iboxMaxMsgID, query, pagination)
   854  	if err != nil {
   855  		s.Debug(ctx, "PullLocalOnly: failed to fetch local messages with iboxMaxMsgID: %v: err %s, trying again with local max", iboxMaxMsgID, err)
   856  		tv, err = s.fetchMaybeNotify(ctx, convID, uid, rc, 0, query, pagination)
   857  		if err != nil {
   858  			s.Debug(ctx, "PullLocalOnly: failed to fetch local messages with local max: %s", err)
   859  			return chat1.ThreadView{}, err
   860  		}
   861  	}
   862  	return tv, nil
   863  }
   864  
   865  func (s *HybridConversationSource) Clear(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID,
   866  	opts *types.ClearOpts) (err error) {
   867  	defer s.Trace(ctx, &err, "Clear(%v,%v)", uid, convID)()
   868  	defer s.PerfTrace(ctx, &err, "Clear(%v,%v)", uid, convID)()
   869  	start := time.Now()
   870  	defer func() {
   871  		var message string
   872  		if err == nil {
   873  			message = fmt.Sprintf("Clearing conv for %s", convID)
   874  		} else {
   875  			message = fmt.Sprintf("Failed to clear conv %s", convID)
   876  		}
   877  		s.G().RuntimeStats.PushPerfEvent(keybase1.PerfEvent{
   878  			EventType: keybase1.PerfEventType_CLEARCONV,
   879  			Message:   message,
   880  			Ctime:     keybase1.ToTime(start),
   881  		})
   882  	}()
   883  	kuid := keybase1.UID(uid.String())
   884  	if (s.G().Env.GetRunMode() == libkb.DevelRunMode || libkb.IsKeybaseAdmin(kuid)) &&
   885  		s.G().UIRouter != nil && opts != nil && opts.SendLocalAdminNotification {
   886  		ui, err := s.G().UIRouter.GetLogUI()
   887  		if err == nil && ui != nil {
   888  			ui.Critical("Clearing conv %s", opts.Reason)
   889  		}
   890  	}
   891  
   892  	epick := libkb.FirstErrorPicker{}
   893  	epick.Push(s.storage.ClearAll(ctx, convID, uid))
   894  	epick.Push(s.G().Indexer.Clear(ctx, uid, convID))
   895  	return epick.Error()
   896  }
   897  
   898  func (s *HybridConversationSource) GetMessages(ctx context.Context, convID chat1.ConversationID,
   899  	uid gregor1.UID, msgIDs []chat1.MessageID, threadReason *chat1.GetThreadReason,
   900  	customRi func() chat1.RemoteInterface, resolveSupersedes bool) (res []chat1.MessageUnboxed, err error) {
   901  	defer s.Trace(ctx, &err, "GetMessages: convID: %s msgIDs: %d",
   902  		convID, len(msgIDs))()
   903  	if _, err := s.lockTab.Acquire(ctx, uid, convID); err != nil {
   904  		return nil, err
   905  	}
   906  	defer s.lockTab.Release(ctx, uid, convID)
   907  	defer s.maybeNuke(ctx, convID, uid, &err)
   908  	defer func() {
   909  		// unless arg says not to, transform the superseded messages
   910  		if !resolveSupersedes {
   911  			return
   912  		}
   913  		res, err = s.TransformSupersedes(ctx, convID, uid, res, nil, nil, nil, nil)
   914  	}()
   915  
   916  	// Grab local messages
   917  	msgs, err := s.storage.FetchMessages(ctx, convID, uid, msgIDs)
   918  	if err != nil {
   919  		return nil, err
   920  	}
   921  
   922  	// Make a pass to determine which message IDs we need to grab remotely
   923  	var remoteMsgs []chat1.MessageID
   924  	for index, msg := range msgs {
   925  		if msg == nil {
   926  			remoteMsgs = append(remoteMsgs, msgIDs[index])
   927  		}
   928  	}
   929  
   930  	// Grab message from remote
   931  	rmsgsTab := make(map[chat1.MessageID]chat1.MessageUnboxed)
   932  	s.Debug(ctx, "GetMessages: convID: %s uid: %s total msgs: %d remote: %d", convID, uid, len(msgIDs),
   933  		len(remoteMsgs))
   934  	if len(remoteMsgs) > 0 {
   935  		rmsgs, err := s.getRi(customRi).GetMessagesRemote(ctx, chat1.GetMessagesRemoteArg{
   936  			ConversationID: convID,
   937  			MessageIDs:     remoteMsgs,
   938  			ThreadReason:   threadReason,
   939  		})
   940  		if err != nil {
   941  			return nil, err
   942  		}
   943  
   944  		// Unbox all the remote messages
   945  		conv := newBasicUnboxConversationInfo(convID, rmsgs.MembersType, nil, rmsgs.Visibility)
   946  		rmsgsUnboxed, err := s.boxer.UnboxMessages(ctx, rmsgs.Msgs, conv)
   947  		if err != nil {
   948  			return nil, err
   949  		}
   950  
   951  		sort.Sort(utils.ByMsgUnboxedMsgID(rmsgsUnboxed))
   952  		for _, rmsg := range rmsgsUnboxed {
   953  			rmsgsTab[rmsg.GetMessageID()] = rmsg
   954  		}
   955  
   956  		reason := chat1.GetThreadReason_GENERAL
   957  		if threadReason != nil {
   958  			reason = *threadReason
   959  		}
   960  		// Write out messages
   961  		if err := s.mergeMaybeNotify(ctx, conv, uid, rmsgsUnboxed, reason); err != nil {
   962  			return nil, err
   963  		}
   964  
   965  		// The localizer uses UnboxQuickMode for unboxing and storing messages. Because of this, if there
   966  		// is a message in the deep past used for something like a channel name, headline, or pin, then we
   967  		// will never actually cache it. Detect this case here and put a load of the messages onto the
   968  		// background loader so we can get these messages cached with the full checks on UnboxMessage.
   969  		if reason == chat1.GetThreadReason_LOCALIZE && globals.CtxUnboxMode(ctx) == types.UnboxModeQuick {
   970  			s.Debug(ctx, "GetMessages: convID: %s remoteMsgs: %d: cache miss on localizer mode with UnboxQuickMode, queuing job", convID, len(remoteMsgs))
   971  			// implement the load entirely in the post load hook since we only want to load those
   972  			// messages in remoteMsgs. We can do that by specifying a 0 length pagination object.
   973  			if err := s.G().ConvLoader.Queue(ctx, types.NewConvLoaderJob(convID, &chat1.Pagination{Num: 0},
   974  				types.ConvLoaderPriorityLowest, types.ConvLoaderUnique,
   975  				func(ctx context.Context, tv chat1.ThreadView, job types.ConvLoaderJob) {
   976  					reason := chat1.GetThreadReason_BACKGROUNDCONVLOAD
   977  					if _, err := s.G().ConvSource.GetMessages(ctx, convID, uid, remoteMsgs, &reason,
   978  						customRi, resolveSupersedes); err != nil {
   979  						s.Debug(ctx, "GetMessages: error loading UnboxQuickMode cache misses: ", err)
   980  					}
   981  
   982  				})); err != nil {
   983  				s.Debug(ctx, "GetMessages: error queuing conv loader job: %+v", err)
   984  			}
   985  		}
   986  	}
   987  
   988  	// Form final result
   989  	for index, msg := range msgs {
   990  		if msg != nil {
   991  			res = append(res, *msg)
   992  		} else {
   993  			res = append(res, rmsgsTab[msgIDs[index]])
   994  		}
   995  	}
   996  	return res, nil
   997  }
   998  
   999  func (s *HybridConversationSource) GetMessagesWithRemotes(ctx context.Context,
  1000  	conv chat1.Conversation, uid gregor1.UID, msgs []chat1.MessageBoxed) (res []chat1.MessageUnboxed, err error) {
  1001  	convID := conv.GetConvID()
  1002  	if _, err := s.lockTab.Acquire(ctx, uid, convID); err != nil {
  1003  		return nil, err
  1004  	}
  1005  	defer s.lockTab.Release(ctx, uid, convID)
  1006  	defer s.maybeNuke(ctx, convID, uid, &err)
  1007  
  1008  	var msgIDs []chat1.MessageID
  1009  	for _, msg := range msgs {
  1010  		msgIDs = append(msgIDs, msg.GetMessageID())
  1011  	}
  1012  
  1013  	lmsgsTab := make(map[chat1.MessageID]chat1.MessageUnboxed)
  1014  
  1015  	lmsgs, err := s.storage.FetchMessages(ctx, convID, uid, msgIDs)
  1016  	if err != nil {
  1017  		return nil, err
  1018  	}
  1019  	for _, lmsg := range lmsgs {
  1020  		if lmsg != nil {
  1021  			lmsgsTab[lmsg.GetMessageID()] = *lmsg
  1022  		}
  1023  	}
  1024  
  1025  	s.Debug(ctx, "GetMessagesWithRemotes: convID: %s uid: %s total msgs: %d hits: %d", convID, uid,
  1026  		len(msgs), len(lmsgsTab))
  1027  	var merges []chat1.MessageUnboxed
  1028  	for _, msg := range msgs {
  1029  		if lmsg, ok := lmsgsTab[msg.GetMessageID()]; ok {
  1030  			res = append(res, lmsg)
  1031  		} else {
  1032  			unboxed, err := s.boxer.UnboxMessage(ctx, msg, conv, nil)
  1033  			if err != nil {
  1034  				return res, err
  1035  			}
  1036  			merges = append(merges, unboxed)
  1037  			res = append(res, unboxed)
  1038  		}
  1039  	}
  1040  	if len(merges) > 0 {
  1041  		sort.Sort(utils.ByMsgUnboxedMsgID(merges))
  1042  		if err := s.mergeMaybeNotify(ctx, utils.RemoteConv(conv), uid, merges, chat1.GetThreadReason_GENERAL); err != nil {
  1043  			return res, err
  1044  		}
  1045  	}
  1046  	sort.Sort(utils.ByMsgUnboxedMsgID(res))
  1047  	return res, nil
  1048  }
  1049  
  1050  func (s *HybridConversationSource) GetUnreadline(ctx context.Context,
  1051  	convID chat1.ConversationID, uid gregor1.UID, readMsgID chat1.MessageID) (unreadlineID *chat1.MessageID, err error) {
  1052  	defer s.Trace(ctx, &err, fmt.Sprintf("GetUnreadline: convID: %v, readMsgID: %v", convID, readMsgID))()
  1053  	defer s.maybeNuke(ctx, convID, uid, &err)
  1054  
  1055  	conv, err := utils.GetUnverifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceLocalOnly)
  1056  	if err != nil { // short circuit to the server
  1057  		s.Debug(ctx, "unable to GetUnverifiedConv: %v", err)
  1058  		return s.getUnreadlineRemote(ctx, convID, readMsgID)
  1059  	}
  1060  	// Don't bother checking anything if we don't have any unread messages.
  1061  	if !conv.Conv.IsUnreadFromMsgID(readMsgID) {
  1062  		return nil, nil
  1063  	}
  1064  
  1065  	unreadlineID, err = storage.New(s.G(), s).FetchUnreadlineID(ctx, convID, uid, readMsgID)
  1066  	if err != nil {
  1067  		return nil, err
  1068  	}
  1069  	if unreadlineID == nil {
  1070  		return s.getUnreadlineRemote(ctx, convID, readMsgID)
  1071  	}
  1072  	return unreadlineID, nil
  1073  }
  1074  
  1075  func (s *HybridConversationSource) notifyExpunge(ctx context.Context, uid gregor1.UID,
  1076  	convID chat1.ConversationID, mergeRes storage.MergeResult) {
  1077  	if mergeRes.Expunged != nil {
  1078  		topicType := chat1.TopicType_NONE
  1079  		conv, err := utils.GetVerifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
  1080  		if err != nil {
  1081  			s.Debug(ctx, "notifyExpunge: failed to get conversations: %s", err)
  1082  		} else {
  1083  			topicType = conv.GetTopicType()
  1084  		}
  1085  		act := chat1.NewChatActivityWithExpunge(chat1.ExpungeInfo{
  1086  			ConvID:  convID,
  1087  			Expunge: *mergeRes.Expunged,
  1088  		})
  1089  		s.G().ActivityNotifier.Activity(ctx, uid, topicType, &act, chat1.ChatActivitySource_LOCAL)
  1090  		s.G().InboxSource.NotifyUpdate(ctx, uid, convID)
  1091  	}
  1092  }
  1093  
  1094  func (s *HybridConversationSource) notifyUpdated(ctx context.Context, uid gregor1.UID,
  1095  	convID chat1.ConversationID, msgs []chat1.MessageUnboxed) {
  1096  	if len(msgs) == 0 {
  1097  		s.Debug(ctx, "notifyUpdated: nothing to do")
  1098  		return
  1099  	}
  1100  	s.Debug(ctx, "notifyUpdated: notifying %d messages", len(msgs))
  1101  	conv, err := utils.GetVerifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
  1102  	if err != nil {
  1103  		s.Debug(ctx, "notifyUpdated: failed to get conv: %s", err)
  1104  		return
  1105  	}
  1106  	maxDeletedUpTo := conv.GetMaxDeletedUpTo()
  1107  	updatedMsgs, err := s.TransformSupersedes(ctx, convID, uid, msgs, nil, nil, nil, &maxDeletedUpTo)
  1108  	if err != nil {
  1109  		s.Debug(ctx, "notifyUpdated: failed to transform supersedes: %s", err)
  1110  		return
  1111  	}
  1112  	s.Debug(ctx, "notifyUpdated: %d messages after transform", len(updatedMsgs))
  1113  	if updatedMsgs, err = NewReplyFiller(s.G()).Fill(ctx, uid, convID, updatedMsgs); err != nil {
  1114  		s.Debug(ctx, "notifyUpdated: failed to fill replies %s", err)
  1115  		return
  1116  	}
  1117  	notif := chat1.MessagesUpdated{
  1118  		ConvID: convID,
  1119  	}
  1120  	for _, msg := range updatedMsgs {
  1121  		notif.Updates = append(notif.Updates, utils.PresentMessageUnboxed(ctx, s.G(), msg, uid, convID))
  1122  	}
  1123  	act := chat1.NewChatActivityWithMessagesUpdated(notif)
  1124  	s.G().ActivityNotifier.Activity(ctx, uid, conv.GetTopicType(),
  1125  		&act, chat1.ChatActivitySource_LOCAL)
  1126  }
  1127  
  1128  // notifyReactionUpdates notifies the GUI after reactions are received
  1129  func (s *HybridConversationSource) notifyReactionUpdates(ctx context.Context, uid gregor1.UID,
  1130  	convID chat1.ConversationID, msgs []chat1.MessageUnboxed) {
  1131  	s.Debug(ctx, "notifyReactionUpdates: %d msgs to update", len(msgs))
  1132  	if len(msgs) > 0 {
  1133  		conv, err := utils.GetVerifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
  1134  		if err != nil {
  1135  			s.Debug(ctx, "notifyReactionUpdates: failed to get conversations: %s", err)
  1136  			return
  1137  		}
  1138  		maxDeletedUpTo := conv.GetMaxDeletedUpTo()
  1139  		msgs, err = s.TransformSupersedes(ctx, convID, uid, msgs, nil, nil, nil, &maxDeletedUpTo)
  1140  		if err != nil {
  1141  			s.Debug(ctx, "notifyReactionUpdates: failed to transform supersedes: %s", err)
  1142  			return
  1143  		}
  1144  		reactionUpdates := []chat1.ReactionUpdate{}
  1145  		for _, msg := range msgs {
  1146  			if msg.IsValid() {
  1147  				d := utils.PresentDecoratedReactionMap(ctx, s.G(), uid, convID, msg.Valid(),
  1148  					msg.Valid().Reactions)
  1149  				reactionUpdates = append(reactionUpdates, chat1.ReactionUpdate{
  1150  					Reactions:   d,
  1151  					TargetMsgID: msg.GetMessageID(),
  1152  				})
  1153  			}
  1154  		}
  1155  		if len(reactionUpdates) > 0 {
  1156  			userReacjis := storage.NewReacjiStore(s.G()).UserReacjis(ctx, uid)
  1157  			activity := chat1.NewChatActivityWithReactionUpdate(chat1.ReactionUpdateNotif{
  1158  				UserReacjis:     userReacjis,
  1159  				ReactionUpdates: reactionUpdates,
  1160  				ConvID:          convID,
  1161  			})
  1162  			s.G().ActivityNotifier.Activity(ctx, uid, conv.GetTopicType(), &activity,
  1163  				chat1.ChatActivitySource_LOCAL)
  1164  		}
  1165  	}
  1166  }
  1167  
  1168  // notifyEphemeralPurge notifies the GUI after messages are exploded.
  1169  func (s *HybridConversationSource) notifyEphemeralPurge(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, explodedMsgs []chat1.MessageUnboxed) {
  1170  	s.Debug(ctx, "notifyEphemeralPurge: exploded: %d", len(explodedMsgs))
  1171  	if len(explodedMsgs) > 0 {
  1172  		// Blast out an EphemeralPurgeNotifInfo since it's time sensitive for the UI
  1173  		// to update.
  1174  		purgedMsgs := []chat1.UIMessage{}
  1175  		for _, msg := range explodedMsgs {
  1176  			purgedMsgs = append(purgedMsgs, utils.PresentMessageUnboxed(ctx, s.G(), msg, uid, convID))
  1177  		}
  1178  		act := chat1.NewChatActivityWithEphemeralPurge(chat1.EphemeralPurgeNotifInfo{
  1179  			ConvID: convID,
  1180  			Msgs:   purgedMsgs,
  1181  		})
  1182  		s.G().ActivityNotifier.Activity(ctx, uid, chat1.TopicType_CHAT, &act, chat1.ChatActivitySource_LOCAL)
  1183  
  1184  		// Send an additional notification to refresh the thread after we bump
  1185  		// the local inbox version
  1186  		s.G().InboxSource.NotifyUpdate(ctx, uid, convID)
  1187  		s.notifyUpdated(ctx, uid, convID, s.storage.GetExplodedReplies(ctx, convID, uid, explodedMsgs))
  1188  	}
  1189  }
  1190  
  1191  // Expunge from storage and maybe notify the gui of staleness
  1192  func (s *HybridConversationSource) Expunge(ctx context.Context,
  1193  	conv types.UnboxConversationInfo, uid gregor1.UID, expunge chat1.Expunge) (err error) {
  1194  	defer s.Trace(ctx, &err, "Expunge")()
  1195  	convID := conv.GetConvID()
  1196  	defer s.maybeNuke(ctx, convID, uid, &err)
  1197  	s.Debug(ctx, "Expunge: convID: %s uid: %s upto: %v", convID, uid, expunge.Upto)
  1198  	if expunge.Upto == 0 {
  1199  		// just get out of here as quickly as possible with a 0 upto
  1200  		return nil
  1201  	}
  1202  	_, err = s.lockTab.Acquire(ctx, uid, convID)
  1203  	if err != nil {
  1204  		return err
  1205  	}
  1206  	defer s.lockTab.Release(ctx, uid, convID)
  1207  	mergeRes, err := s.storage.Expunge(ctx, conv, uid, expunge)
  1208  	if err != nil {
  1209  		return err
  1210  	}
  1211  	s.notifyExpunge(ctx, uid, convID, mergeRes)
  1212  	return nil
  1213  }
  1214  
  1215  // Merge with storage and maybe notify the gui of staleness
  1216  func (s *HybridConversationSource) mergeMaybeNotify(ctx context.Context,
  1217  	conv types.UnboxConversationInfo, uid gregor1.UID, msgs []chat1.MessageUnboxed, reason chat1.GetThreadReason) error {
  1218  	convID := conv.GetConvID()
  1219  	switch globals.CtxUnboxMode(ctx) {
  1220  	case types.UnboxModeFull:
  1221  		s.Debug(ctx, "mergeMaybeNotify: full mode, merging %d messages", len(msgs))
  1222  	case types.UnboxModeQuick:
  1223  		s.Debug(ctx, "mergeMaybeNotify: quick mode, skipping %d messages", len(msgs))
  1224  		globals.CtxAddMessageCacheSkips(ctx, convID, msgs)
  1225  		return nil
  1226  	}
  1227  
  1228  	mergeRes, err := s.storage.Merge(ctx, conv, uid, msgs)
  1229  	if err != nil {
  1230  		return err
  1231  	}
  1232  
  1233  	// skip notifications during background loads
  1234  	if reason == chat1.GetThreadReason_BACKGROUNDCONVLOAD {
  1235  		return nil
  1236  	}
  1237  
  1238  	var unfurlTargets []chat1.MessageUnboxed
  1239  	for _, r := range mergeRes.UnfurlTargets {
  1240  		if r.IsMapDelete {
  1241  			// we don't tell the UI about map deletes so they don't jump in and out
  1242  			continue
  1243  		}
  1244  		unfurlTargets = append(unfurlTargets, r.Msg)
  1245  	}
  1246  	s.notifyExpunge(ctx, uid, convID, mergeRes)
  1247  	s.notifyEphemeralPurge(ctx, uid, convID, mergeRes.Exploded)
  1248  	s.notifyReactionUpdates(ctx, uid, convID, mergeRes.ReactionTargets)
  1249  	s.notifyUpdated(ctx, uid, convID, unfurlTargets)
  1250  	s.notifyUpdated(ctx, uid, convID, mergeRes.RepliesAffected)
  1251  	return nil
  1252  }
  1253  
  1254  func (s *HybridConversationSource) fetchMaybeNotify(ctx context.Context, convID chat1.ConversationID,
  1255  	uid gregor1.UID, rc storage.ResultCollector, maxMsgID chat1.MessageID, query *chat1.GetThreadQuery,
  1256  	pagination *chat1.Pagination) (tv chat1.ThreadView, err error) {
  1257  
  1258  	fetchRes, err := s.storage.FetchUpToLocalMaxMsgID(ctx, convID, uid, rc, maxMsgID,
  1259  		query, pagination)
  1260  	if err != nil {
  1261  		return tv, err
  1262  	}
  1263  	s.notifyEphemeralPurge(ctx, uid, convID, fetchRes.Exploded)
  1264  	return fetchRes.Thread, nil
  1265  }
  1266  
  1267  func (s *HybridConversationSource) EphemeralPurge(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID,
  1268  	purgeInfo *chat1.EphemeralPurgeInfo) (newPurgeInfo *chat1.EphemeralPurgeInfo, explodedMsgs []chat1.MessageUnboxed, err error) {
  1269  	defer s.Trace(ctx, &err, "EphemeralPurge")()
  1270  	defer s.maybeNuke(ctx, convID, uid, &err)
  1271  	if newPurgeInfo, explodedMsgs, err = s.storage.EphemeralPurge(ctx, convID, uid, purgeInfo); err != nil {
  1272  		return newPurgeInfo, explodedMsgs, err
  1273  	}
  1274  	s.notifyEphemeralPurge(ctx, uid, convID, explodedMsgs)
  1275  	return newPurgeInfo, explodedMsgs, nil
  1276  }
  1277  
  1278  func NewConversationSource(g *globals.Context, typ string, boxer *Boxer, storage *storage.Storage,
  1279  	ri func() chat1.RemoteInterface) types.ConversationSource {
  1280  	if typ == "hybrid" {
  1281  		return NewHybridConversationSource(g, boxer, storage, ri)
  1282  	}
  1283  	return NewRemoteConversationSource(g, boxer, ri)
  1284  }