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