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

     1  package storage
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sort"
     7  
     8  	"github.com/keybase/client/go/chat/globals"
     9  	"github.com/keybase/client/go/chat/pager"
    10  	"github.com/keybase/client/go/chat/types"
    11  	"github.com/keybase/client/go/chat/utils"
    12  	"github.com/keybase/client/go/libkb"
    13  	"github.com/keybase/client/go/protocol/chat1"
    14  	"github.com/keybase/client/go/protocol/gregor1"
    15  	"github.com/keybase/client/go/protocol/keybase1"
    16  	"github.com/keybase/clockwork"
    17  	"github.com/keybase/go-codec/codec"
    18  	"golang.org/x/net/context"
    19  )
    20  
    21  var maxFetchNum = 1000
    22  
    23  type ResultCollector interface {
    24  	Push(msg chat1.MessageUnboxed)
    25  	PushPlaceholder(msgID chat1.MessageID) bool
    26  	Done() bool
    27  	Result() []chat1.MessageUnboxed
    28  	Error(err Error) Error
    29  	Name() string
    30  	SetTarget(num int)
    31  
    32  	String() string
    33  }
    34  
    35  type Storage struct {
    36  	globals.Contextified
    37  	utils.DebugLabeler
    38  
    39  	engine       storageEngine
    40  	idtracker    *msgIDTracker
    41  	breakTracker *breakTracker
    42  	delhTracker  *delhTracker
    43  	assetDeleter AssetDeleter
    44  	clock        clockwork.Clock
    45  }
    46  
    47  type storageEngine interface {
    48  	Init(ctx context.Context, key [32]byte, convID chat1.ConversationID,
    49  		uid gregor1.UID) (context.Context, Error)
    50  	WriteMessages(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID,
    51  		msgs []chat1.MessageUnboxed) Error
    52  	ReadMessages(ctx context.Context, res ResultCollector,
    53  		convID chat1.ConversationID, uid gregor1.UID, maxID, minID chat1.MessageID) Error
    54  	ClearMessages(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID,
    55  		msgIDs []chat1.MessageID) Error
    56  }
    57  
    58  type AssetDeleter interface {
    59  	DeleteAssets(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, assets []chat1.Asset)
    60  }
    61  
    62  type DummyAssetDeleter struct{}
    63  
    64  func (d DummyAssetDeleter) DeleteAssets(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
    65  	assets []chat1.Asset) {
    66  
    67  }
    68  
    69  func New(g *globals.Context, assetDeleter AssetDeleter) *Storage {
    70  	return &Storage{
    71  		Contextified: globals.NewContextified(g),
    72  		engine:       newBlockEngine(g),
    73  		idtracker:    newMsgIDTracker(g),
    74  		breakTracker: newBreakTracker(g),
    75  		delhTracker:  newDelhTracker(g),
    76  		assetDeleter: assetDeleter,
    77  		clock:        clockwork.NewRealClock(),
    78  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Storage", false),
    79  	}
    80  }
    81  
    82  func (s *Storage) setEngine(engine storageEngine) {
    83  	s.engine = engine
    84  }
    85  
    86  func (s *Storage) SetClock(clock clockwork.Clock) {
    87  	s.clock = clock
    88  }
    89  
    90  func (s *Storage) SetAssetDeleter(assetDeleter AssetDeleter) {
    91  	s.assetDeleter = assetDeleter
    92  }
    93  
    94  func makeBlockIndexKey(convID chat1.ConversationID, uid gregor1.UID) libkb.DbKey {
    95  	return libkb.DbKey{
    96  		Typ: libkb.DBChatBlockIndex,
    97  		Key: fmt.Sprintf("bi:%s:%s", uid, convID),
    98  	}
    99  }
   100  
   101  func encode(input interface{}) ([]byte, error) {
   102  	mh := codec.MsgpackHandle{WriteExt: true}
   103  	var data []byte
   104  	enc := codec.NewEncoderBytes(&data, &mh)
   105  	if err := enc.Encode(input); err != nil {
   106  		return nil, err
   107  	}
   108  	return data, nil
   109  }
   110  
   111  func decode(data []byte, res interface{}) error {
   112  	mh := codec.MsgpackHandle{WriteExt: true}
   113  	dec := codec.NewDecoderBytes(data, &mh)
   114  	err := dec.Decode(res)
   115  	return err
   116  }
   117  
   118  // SimpleResultCollector aggregates all results in a basic way. It is not thread safe.
   119  type SimpleResultCollector struct {
   120  	res                  []chat1.MessageUnboxed
   121  	target, cur, curScan int
   122  
   123  	// countAll controls whether or not deleted messages should count toward target
   124  	countAll bool
   125  }
   126  
   127  var _ ResultCollector = (*SimpleResultCollector)(nil)
   128  
   129  func (s *SimpleResultCollector) Push(msg chat1.MessageUnboxed) {
   130  	s.res = append(s.res, msg)
   131  	if s.countAll || !msg.IsValidDeleted() {
   132  		s.cur++
   133  	}
   134  	s.curScan++
   135  }
   136  
   137  func (s *SimpleResultCollector) Done() bool {
   138  	if s.target < 0 {
   139  		return false
   140  	}
   141  	return s.cur >= s.target || s.curScan >= maxFetchNum
   142  }
   143  
   144  func (s *SimpleResultCollector) Result() []chat1.MessageUnboxed {
   145  	return s.res
   146  }
   147  
   148  func (s *SimpleResultCollector) Name() string {
   149  	return "simple"
   150  }
   151  
   152  func (s *SimpleResultCollector) String() string {
   153  	return fmt.Sprintf("[ %s: t: %d c: %d ]", s.Name(), s.target, len(s.res))
   154  }
   155  
   156  func (s *SimpleResultCollector) Error(err Error) Error {
   157  	if s.target < 0 {
   158  		// Swallow this error if we are not looking for a target
   159  		if _, ok := err.(MissError); ok {
   160  			return nil
   161  		}
   162  	}
   163  	return err
   164  }
   165  
   166  func (s *SimpleResultCollector) PushPlaceholder(chat1.MessageID) bool {
   167  	return false
   168  }
   169  
   170  func (s *SimpleResultCollector) SetTarget(num int) {
   171  	s.target = num
   172  }
   173  
   174  func NewSimpleResultCollector(num int, countAll bool) *SimpleResultCollector {
   175  	return &SimpleResultCollector{
   176  		target:   num,
   177  		countAll: countAll,
   178  	}
   179  }
   180  
   181  type InsatiableResultCollector struct {
   182  	res []chat1.MessageUnboxed
   183  }
   184  
   185  var _ ResultCollector = (*InsatiableResultCollector)(nil)
   186  
   187  // InsatiableResultCollector aggregates all messages all the way back.
   188  // Its result can include holes.
   189  func NewInsatiableResultCollector() *InsatiableResultCollector {
   190  	return &InsatiableResultCollector{}
   191  }
   192  
   193  func (s *InsatiableResultCollector) Push(msg chat1.MessageUnboxed) {
   194  	s.res = append(s.res, msg)
   195  }
   196  
   197  func (s *InsatiableResultCollector) Done() bool {
   198  	return false
   199  }
   200  
   201  func (s *InsatiableResultCollector) Result() []chat1.MessageUnboxed {
   202  	return s.res
   203  }
   204  
   205  func (s *InsatiableResultCollector) Name() string {
   206  	return "inf"
   207  }
   208  
   209  func (s *InsatiableResultCollector) String() string {
   210  	return fmt.Sprintf("[ %s: c: %d ]", s.Name(), len(s.res))
   211  }
   212  
   213  func (s *InsatiableResultCollector) Error(err Error) Error {
   214  	return err
   215  }
   216  
   217  func (s *InsatiableResultCollector) SetTarget(num int) {}
   218  
   219  func (s *InsatiableResultCollector) PushPlaceholder(chat1.MessageID) bool {
   220  	// Missing messages are a-ok
   221  	return true
   222  }
   223  
   224  // TypedResultCollector aggregates results with a type constraints. It is not thread safe.
   225  type TypedResultCollector struct {
   226  	res                  []chat1.MessageUnboxed
   227  	target, cur, curScan int
   228  	typmap               map[chat1.MessageType]bool
   229  }
   230  
   231  var _ ResultCollector = (*TypedResultCollector)(nil)
   232  
   233  func NewTypedResultCollector(num int, typs []chat1.MessageType) *TypedResultCollector {
   234  	c := TypedResultCollector{
   235  		target: num,
   236  		typmap: make(map[chat1.MessageType]bool),
   237  	}
   238  	for _, typ := range typs {
   239  		c.typmap[typ] = true
   240  	}
   241  	return &c
   242  }
   243  
   244  func (t *TypedResultCollector) Push(msg chat1.MessageUnboxed) {
   245  	t.res = append(t.res, msg)
   246  	if !msg.IsValidDeleted() && t.typmap[msg.GetMessageType()] {
   247  		t.cur++
   248  	}
   249  	t.curScan++
   250  }
   251  
   252  func (t *TypedResultCollector) Done() bool {
   253  	if t.target < 0 {
   254  		return false
   255  	}
   256  	return t.cur >= t.target || t.curScan >= maxFetchNum
   257  }
   258  
   259  func (t *TypedResultCollector) Result() []chat1.MessageUnboxed {
   260  	return t.res
   261  }
   262  
   263  func (t *TypedResultCollector) Name() string {
   264  	return "typed"
   265  }
   266  
   267  func (t *TypedResultCollector) String() string {
   268  	return fmt.Sprintf("[ %s: t: %d c: %d (%d types) ]", t.Name(), t.target, t.cur, len(t.typmap))
   269  }
   270  
   271  func (t *TypedResultCollector) Error(err Error) Error {
   272  	if t.target < 0 {
   273  		// Swallow this error if we are not looking for a target
   274  		if _, ok := err.(MissError); ok {
   275  			return nil
   276  		}
   277  	}
   278  	return err
   279  }
   280  
   281  func (t *TypedResultCollector) PushPlaceholder(msgID chat1.MessageID) bool {
   282  	return false
   283  }
   284  
   285  func (t *TypedResultCollector) SetTarget(num int) {
   286  	t.target = num
   287  }
   288  
   289  type HoleyResultCollector struct {
   290  	ResultCollector
   291  
   292  	maxHoles, holes int
   293  }
   294  
   295  var _ ResultCollector = (*HoleyResultCollector)(nil)
   296  
   297  func NewHoleyResultCollector(maxHoles int, rc ResultCollector) *HoleyResultCollector {
   298  	return &HoleyResultCollector{
   299  		ResultCollector: rc,
   300  		maxHoles:        maxHoles,
   301  	}
   302  }
   303  
   304  func (h *HoleyResultCollector) PushPlaceholder(msgID chat1.MessageID) bool {
   305  	if h.holes >= h.maxHoles {
   306  		return false
   307  	}
   308  
   309  	h.ResultCollector.Push(chat1.NewMessageUnboxedWithPlaceholder(chat1.MessageUnboxedPlaceholder{
   310  		MessageID: msgID,
   311  	}))
   312  	h.holes++
   313  	return true
   314  }
   315  
   316  func (h *HoleyResultCollector) Holes() int {
   317  	return h.holes
   318  }
   319  
   320  func (s *Storage) castInternalError(ierr Error) error {
   321  	err, ok := ierr.(error)
   322  	if ok {
   323  		return err
   324  	}
   325  	return nil
   326  }
   327  
   328  func (s *Storage) Nuke(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID) Error {
   329  	lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), convID.String())
   330  	defer lock.Release(ctx)
   331  	return s.maybeNukeLocked(ctx, true /* force */, nil /* error */, convID, uid)
   332  }
   333  
   334  func (s *Storage) maybeNukeLocked(ctx context.Context, force bool, err Error, convID chat1.ConversationID,
   335  	uid gregor1.UID) Error {
   336  	// Clear index
   337  	if force || err.ShouldClear() {
   338  		s.Debug(ctx, "chat local storage corrupted: clearing")
   339  		if err := s.G().LocalChatDb.Delete(makeBlockIndexKey(convID, uid)); err != nil {
   340  			s.Debug(ctx, "failed to delete chat index, clearing entire local storage (delete error: %s)",
   341  				err)
   342  			if _, err = s.G().LocalChatDb.Nuke(); err != nil {
   343  				s.Debug(ctx, "failed to delete chat local storage: %s", err)
   344  			}
   345  		}
   346  		if err := s.idtracker.clear(convID, uid); err != nil {
   347  			s.Debug(ctx, "failed to clear max message storage: %s", err)
   348  		}
   349  		if err := s.G().EphemeralTracker.Clear(ctx, convID, uid); err != nil {
   350  			s.Debug(ctx, "failed to clear ephemeral tracker storage: %s", err)
   351  		}
   352  	}
   353  	return err
   354  }
   355  
   356  func (s *Storage) SetMaxMsgID(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID,
   357  	msgID chat1.MessageID) (err Error) {
   358  	var ierr error
   359  	defer s.Trace(ctx, &ierr, "SetMaxMsgID")()
   360  	defer func() { ierr = s.castInternalError(err) }()
   361  	lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), convID.String())
   362  	defer lock.Release(ctx)
   363  	return s.idtracker.bumpMaxMessageID(ctx, convID, uid, msgID)
   364  }
   365  
   366  func (s *Storage) GetMaxMsgID(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID) (maxMsgID chat1.MessageID, err Error) {
   367  	var ierr error
   368  	defer s.Trace(ctx, &ierr, "GetMaxMsgID")()
   369  	defer func() { ierr = s.castInternalError(err) }()
   370  	lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), convID.String())
   371  	defer lock.Release(ctx)
   372  
   373  	if maxMsgID, err = s.idtracker.getMaxMessageID(ctx, convID, uid); err != nil {
   374  		return maxMsgID, s.maybeNukeLocked(ctx, false, err, convID, uid)
   375  	}
   376  	return maxMsgID, nil
   377  }
   378  
   379  type UnfurlMergeResult struct {
   380  	Msg         chat1.MessageUnboxed
   381  	IsMapDelete bool
   382  }
   383  
   384  type MergeResult struct {
   385  	Expunged        *chat1.Expunge
   386  	Exploded        []chat1.MessageUnboxed
   387  	ReactionTargets []chat1.MessageUnboxed
   388  	UnfurlTargets   []UnfurlMergeResult
   389  	RepliesAffected []chat1.MessageUnboxed
   390  }
   391  
   392  type FetchResult struct {
   393  	Thread   chat1.ThreadView
   394  	Exploded []chat1.MessageUnboxed
   395  }
   396  
   397  // Merge requires msgs to be sorted by descending message ID
   398  func (s *Storage) Merge(ctx context.Context,
   399  	conv types.UnboxConversationInfo, uid gregor1.UID, msgs []chat1.MessageUnboxed) (res MergeResult, err Error) {
   400  	var ierr error
   401  	defer s.Trace(ctx, &ierr, "Merge")()
   402  	defer func() { ierr = s.castInternalError(err) }()
   403  	return s.MergeHelper(ctx, conv, uid, msgs, nil)
   404  }
   405  
   406  func (s *Storage) Expunge(ctx context.Context,
   407  	conv types.UnboxConversationInfo, uid gregor1.UID, expunge chat1.Expunge) (res MergeResult, err Error) {
   408  	var ierr error
   409  	defer s.Trace(ctx, &ierr, "Expunge")()
   410  	defer func() { ierr = s.castInternalError(err) }()
   411  	// Merge with no messages, just the expunge.
   412  	return s.MergeHelper(ctx, conv, uid, nil, &expunge)
   413  }
   414  
   415  // MergeHelper requires msgs to be sorted by descending message ID
   416  // expunge is optional
   417  func (s *Storage) MergeHelper(ctx context.Context,
   418  	conv types.UnboxConversationInfo, uid gregor1.UID, msgs []chat1.MessageUnboxed, expunge *chat1.Expunge) (res MergeResult, err Error) {
   419  	var ierr error
   420  	defer s.Trace(ctx, &ierr, "MergeHelper")()
   421  	defer func() { ierr = s.castInternalError(err) }()
   422  	convID := conv.GetConvID()
   423  	lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), convID.String())
   424  	defer lock.Release(ctx)
   425  
   426  	s.Debug(ctx, "MergeHelper: convID: %s uid: %s num msgs: %d", convID, uid, len(msgs))
   427  
   428  	// Fetch secret key
   429  	key, ierr := GetSecretBoxKey(ctx, s.G().ExternalG())
   430  	if ierr != nil {
   431  		return res, MiscError{Msg: "unable to get secret key: " + ierr.Error()}
   432  	}
   433  
   434  	ctx, err = s.engine.Init(ctx, key, convID, uid)
   435  	if err != nil {
   436  		return res, err
   437  	}
   438  
   439  	// Write out new data into blocks
   440  	if err = s.engine.WriteMessages(ctx, convID, uid, msgs); err != nil {
   441  		return res, s.maybeNukeLocked(ctx, false, err, convID, uid)
   442  	}
   443  
   444  	// Update supersededBy pointers
   445  	updateRes, err := s.updateAllSupersededBy(ctx, convID, uid, msgs)
   446  	if err != nil {
   447  		return res, s.maybeNukeLocked(ctx, false, err, convID, uid)
   448  	}
   449  	res.ReactionTargets = updateRes.reactionTargets
   450  	res.UnfurlTargets = updateRes.unfurlTargets
   451  	res.RepliesAffected = updateRes.repliesAffected
   452  
   453  	if err = s.updateMinDeletableMessage(ctx, convID, uid, msgs); err != nil {
   454  		return res, s.maybeNukeLocked(ctx, false, err, convID, uid)
   455  	}
   456  
   457  	// Process any DeleteHistory messages
   458  	expunged, err := s.handleDeleteHistory(ctx, conv, uid, msgs, expunge)
   459  	if err != nil {
   460  		return res, s.maybeNukeLocked(ctx, false, err, convID, uid)
   461  	}
   462  	res.Expunged = expunged
   463  
   464  	exploded, err := s.explodeExpiredMessages(ctx, convID, uid, msgs)
   465  	if err != nil {
   466  		return res, s.maybeNukeLocked(ctx, false, err, convID, uid)
   467  	}
   468  	res.Exploded = exploded
   469  
   470  	// Update max msg ID if needed
   471  	if len(msgs) > 0 {
   472  		if err := s.idtracker.bumpMaxMessageID(ctx, convID, uid, msgs[0].GetMessageID()); err != nil {
   473  			return res, s.maybeNukeLocked(ctx, false, err, convID, uid)
   474  		}
   475  	}
   476  
   477  	// queue search index update in the background
   478  	go func(ctx context.Context) {
   479  		err := s.G().Indexer.Add(ctx, convID, msgs)
   480  		if err != nil {
   481  			s.Debug(ctx, "Error adding to indexer: %+v", err)
   482  		}
   483  	}(globals.BackgroundChatCtx(ctx, s.G()))
   484  
   485  	return res, nil
   486  }
   487  
   488  type updateAllSupersededByRes struct {
   489  	reactionTargets []chat1.MessageUnboxed
   490  	unfurlTargets   []UnfurlMergeResult
   491  	repliesAffected []chat1.MessageUnboxed
   492  }
   493  
   494  func (s *Storage) isReply(msg chat1.MessageUnboxed) *chat1.MessageID {
   495  	if !msg.IsValid() {
   496  		return nil
   497  	}
   498  	body := msg.Valid().MessageBody
   499  	if body.IsType(chat1.MessageType_TEXT) && body.Text().ReplyTo != nil && *body.Text().ReplyTo > 0 {
   500  		return body.Text().ReplyTo
   501  	}
   502  	return nil
   503  }
   504  
   505  func (s *Storage) updateAllSupersededBy(ctx context.Context, convID chat1.ConversationID,
   506  	uid gregor1.UID, inMsgs []chat1.MessageUnboxed) (res updateAllSupersededByRes, err Error) {
   507  	s.Debug(ctx, "updateSupersededBy: num msgs: %d", len(inMsgs))
   508  	// Do a pass over all the messages and update supersededBy pointers
   509  
   510  	var allAssets []chat1.Asset
   511  	var allPurged []chat1.MessageUnboxed
   512  	// We return a set of reaction targets that have been updated
   513  	updatedReactionTargets := map[chat1.MessageID]chat1.MessageUnboxed{}
   514  	// Unfurl targets
   515  	updatedUnfurlTargets := make(map[chat1.MessageID]UnfurlMergeResult)
   516  	repliesAffected := map[chat1.MessageID]chat1.MessageUnboxed{}
   517  
   518  	// Sort in reverse order so this playback works as it would have if we received these
   519  	// in real-time
   520  	msgs := make([]chat1.MessageUnboxed, len(inMsgs))
   521  	copy(msgs, inMsgs)
   522  	sort.Slice(msgs, func(i, j int) bool {
   523  		return msgs[i].GetMessageID() < msgs[j].GetMessageID()
   524  	})
   525  	newMsgMap := make(map[chat1.MessageID]chat1.MessageUnboxed)
   526  	getMessage := func(msgID chat1.MessageID) (*chat1.MessageUnboxed, Error) {
   527  		stored, ok := newMsgMap[msgID]
   528  		if ok {
   529  			return &stored, nil
   530  		}
   531  		return s.getMessage(ctx, convID, uid, msgID)
   532  	}
   533  	for _, msg := range msgs {
   534  		msgid := msg.GetMessageID()
   535  		if !msg.IsValid() {
   536  			s.Debug(ctx, "updateSupersededBy: skipping potential superseder marked as not valid: %v", msg.DebugString())
   537  			continue
   538  		}
   539  
   540  		supersededIDs, ierr := utils.GetSupersedes(msg)
   541  		if ierr != nil {
   542  			continue
   543  		}
   544  		if replyID := s.isReply(msg); replyID != nil {
   545  			supersededIDs = append(supersededIDs, *replyID)
   546  		}
   547  		// Set all supersedes targets
   548  		for _, supersededID := range supersededIDs {
   549  			if supersededID == 0 {
   550  				s.Debug(ctx, "updateSupersededBy: skipping invalid supersededID: %v for msg: %v", supersededID, msg.DebugString())
   551  				continue
   552  			}
   553  
   554  			s.Debug(ctx, "updateSupersededBy: msg: %v supersedes: %v", msg.DebugString(), supersededID)
   555  			// Read superseded msg
   556  			superMsg, err := getMessage(supersededID)
   557  			if err != nil {
   558  				return res, err
   559  			}
   560  			if superMsg == nil {
   561  				continue
   562  			}
   563  
   564  			// Update supersededBy and reactionIDs on the target message if we
   565  			// have it. If the superseder is a deletion, delete the body as
   566  			// well. If we are deleting a reaction, update the reaction's
   567  			// target message.
   568  			if superMsg.IsValid() {
   569  				s.Debug(ctx, "updateSupersededBy: writing: id: %d superseded: %d", msgid, supersededID)
   570  				mvalid := superMsg.Valid()
   571  
   572  				switch msg.GetMessageType() {
   573  				case chat1.MessageType_TEXT:
   574  					mvalid.ServerHeader.Replies = append(mvalid.ServerHeader.Replies, msg.GetMessageID())
   575  					newMsg := chat1.NewMessageUnboxedWithValid(mvalid)
   576  					newMsgMap[newMsg.GetMessageID()] = newMsg
   577  				case chat1.MessageType_UNFURL:
   578  					unfurl := msg.Valid().MessageBody.Unfurl()
   579  					utils.SetUnfurl(&mvalid, msg.GetMessageID(), unfurl.Unfurl)
   580  					newMsg := chat1.NewMessageUnboxedWithValid(mvalid)
   581  					newMsgMap[newMsg.GetMessageID()] = newMsg
   582  					updatedUnfurlTargets[superMsg.GetMessageID()] = UnfurlMergeResult{
   583  						Msg:         newMsg,
   584  						IsMapDelete: false,
   585  					}
   586  				case chat1.MessageType_REACTION:
   587  					// If we haven't modified any reaction data, we don't want
   588  					// to send it up for a notification.
   589  					var reactionUpdate bool
   590  					// reactions don't update SupersededBy, instead they rely
   591  					// on ReactionIDs
   592  					mvalid.ServerHeader.ReactionIDs, reactionUpdate =
   593  						s.updateReactionIDs(mvalid.ServerHeader.ReactionIDs, msgid)
   594  					newMsg := chat1.NewMessageUnboxedWithValid(mvalid)
   595  					newMsgMap[newMsg.GetMessageID()] = newMsg
   596  					if reactionUpdate {
   597  						updatedReactionTargets[superMsg.GetMessageID()] = newMsg
   598  					}
   599  				case chat1.MessageType_DELETE:
   600  					mvalid.ServerHeader.SupersededBy = msgid
   601  					s.updateRepliesAffected(ctx, convID, uid, mvalid.ServerHeader.Replies, repliesAffected)
   602  					switch superMsg.GetMessageType() {
   603  					case chat1.MessageType_UNFURL:
   604  						updatedTarget, err := s.updateUnfurlTargetOnDelete(ctx, convID, uid, *superMsg)
   605  						if err != nil {
   606  							s.Debug(ctx, "updateSupersededBy: failed to update unfurl target: %s", err)
   607  						} else {
   608  							updatedUnfurlTargets[updatedTarget.GetMessageID()] = UnfurlMergeResult{
   609  								Msg:         updatedTarget,
   610  								IsMapDelete: utils.IsMapUnfurl(*superMsg),
   611  							}
   612  							newMsgMap[updatedTarget.GetMessageID()] = updatedTarget
   613  						}
   614  					case chat1.MessageType_REACTION:
   615  						// We have to find the message we are reacting to and
   616  						// update it's ReactionIDs as well.
   617  						newTargetMsg, reactionUpdate, err := s.updateReactionTargetOnDelete(ctx, convID, uid,
   618  							superMsg)
   619  						if err != nil {
   620  							return res, err
   621  						} else if newTargetMsg != nil {
   622  							if reactionUpdate {
   623  								updatedReactionTargets[newTargetMsg.GetMessageID()] = *newTargetMsg
   624  							}
   625  							newMsgMap[newTargetMsg.GetMessageID()] = *newTargetMsg
   626  						}
   627  					}
   628  					msgPurged, assets := s.purgeMessage(mvalid)
   629  					allPurged = append(allPurged, *superMsg)
   630  					allAssets = append(allAssets, assets...)
   631  					newMsgMap[msgPurged.GetMessageID()] = msgPurged
   632  				case chat1.MessageType_EDIT:
   633  					s.updateRepliesAffected(ctx, convID, uid, mvalid.ServerHeader.Replies, repliesAffected)
   634  					fallthrough
   635  				default:
   636  					mvalid.ServerHeader.SupersededBy = msgid
   637  					newMsg := chat1.NewMessageUnboxedWithValid(mvalid)
   638  					newMsgMap[newMsg.GetMessageID()] = newMsg
   639  				}
   640  			} else {
   641  				s.Debug(ctx, "updateSupersededBy: skipping id: %d, it is stored as an error",
   642  					superMsg.GetMessageID())
   643  			}
   644  		}
   645  	}
   646  
   647  	// Write out all the modified messages in one shot
   648  	newMsgs := make([]chat1.MessageUnboxed, 0, len(newMsgMap))
   649  	for _, msg := range newMsgMap {
   650  		newMsgs = append(newMsgs, msg)
   651  	}
   652  	sort.Slice(newMsgs, func(i, j int) bool {
   653  		return newMsgs[i].GetMessageID() > newMsgs[j].GetMessageID()
   654  	})
   655  	if err = s.engine.WriteMessages(ctx, convID, uid, newMsgs); err != nil {
   656  		return res, err
   657  	}
   658  
   659  	// queue asset deletions in the background
   660  	s.assetDeleter.DeleteAssets(ctx, uid, convID, allAssets)
   661  	// queue search index update in the background
   662  	go func(ctx context.Context) {
   663  		err := s.G().Indexer.Remove(ctx, convID, allPurged)
   664  		if err != nil {
   665  			s.Debug(ctx, "Error removing from indexer: %+v", err)
   666  		}
   667  	}(globals.BackgroundChatCtx(ctx, s.G()))
   668  	var flattenedUnfurlTargets []UnfurlMergeResult
   669  	for _, r := range updatedUnfurlTargets {
   670  		flattenedUnfurlTargets = append(flattenedUnfurlTargets, r)
   671  	}
   672  	return updateAllSupersededByRes{
   673  		reactionTargets: s.flatten(updatedReactionTargets),
   674  		unfurlTargets:   flattenedUnfurlTargets,
   675  		repliesAffected: s.flatten(repliesAffected),
   676  	}, nil
   677  }
   678  
   679  func (s *Storage) flatten(m map[chat1.MessageID]chat1.MessageUnboxed) (res []chat1.MessageUnboxed) {
   680  	for _, msg := range m {
   681  		res = append(res, msg)
   682  	}
   683  	return res
   684  }
   685  
   686  func (s *Storage) updateMinDeletableMessage(ctx context.Context, convID chat1.ConversationID,
   687  	uid gregor1.UID, msgs []chat1.MessageUnboxed) Error {
   688  
   689  	de := func(format string, args ...interface{}) {
   690  		s.Debug(ctx, "updateMinDeletableMessage: "+fmt.Sprintf(format, args...))
   691  	}
   692  
   693  	// The min deletable message ID in this new batch of messages.
   694  	var minDeletableMessageBatch *chat1.MessageID
   695  	for _, msg := range msgs {
   696  		msgid := msg.GetMessageID()
   697  		if !msg.IsValid() {
   698  			continue
   699  		}
   700  		if !chat1.IsDeletableByDeleteHistory(msg.GetMessageType()) {
   701  			continue
   702  		}
   703  		if msg.Valid().MessageBody.IsNil() {
   704  			continue
   705  		}
   706  		if minDeletableMessageBatch == nil || msgid < *minDeletableMessageBatch {
   707  			minDeletableMessageBatch = &msgid
   708  		}
   709  	}
   710  
   711  	// Update the tracker to min(mem, batch)
   712  	if minDeletableMessageBatch != nil {
   713  		mem, err := s.delhTracker.getEntry(ctx, convID, uid)
   714  		switch err.(type) {
   715  		case nil:
   716  			if mem.MinDeletableMessage > 0 && *minDeletableMessageBatch >= mem.MinDeletableMessage {
   717  				// no need to update
   718  				return nil
   719  			}
   720  		case MissError:
   721  			// We have no memory
   722  		default:
   723  			return err
   724  		}
   725  
   726  		err = s.delhTracker.setMinDeletableMessage(ctx, convID, uid, *minDeletableMessageBatch)
   727  		if err != nil {
   728  			de("failed to store delh track: %v", err)
   729  		}
   730  	}
   731  
   732  	return nil
   733  }
   734  
   735  // Apply any new DeleteHistory from msgs.
   736  // Returns a non-nil expunge if deletes happened.
   737  // Shortcircuits so it's ok to call a lot.
   738  // The actual effect will be to delete upto the max of `expungeExplicit` (which can be nil)
   739  //
   740  //	and the DeleteHistory-type messages.
   741  func (s *Storage) handleDeleteHistory(ctx context.Context, conv types.UnboxConversationInfo,
   742  	uid gregor1.UID, msgs []chat1.MessageUnboxed, expungeExplicit *chat1.Expunge) (*chat1.Expunge, Error) {
   743  
   744  	de := func(format string, args ...interface{}) {
   745  		s.Debug(ctx, "handleDeleteHistory: "+fmt.Sprintf(format, args...))
   746  	}
   747  
   748  	// Find the DeleteHistory message with the maximum upto value.
   749  	expungeActive := expungeExplicit
   750  	for _, msg := range msgs {
   751  		msgid := msg.GetMessageID()
   752  		if !msg.IsValid() {
   753  			de("skipping message marked as not valid: %v", msg.DebugString())
   754  			continue
   755  		}
   756  		if msg.GetMessageType() != chat1.MessageType_DELETEHISTORY {
   757  			continue
   758  		}
   759  		mvalid := msg.Valid()
   760  		bodyType, err := mvalid.MessageBody.MessageType()
   761  		if err != nil {
   762  			de("skipping corrupted message body: %v", err)
   763  			continue
   764  		}
   765  		if bodyType != chat1.MessageType_DELETEHISTORY {
   766  			de("skipping wrong message body type: %v", err)
   767  			continue
   768  		}
   769  		delh := mvalid.MessageBody.Deletehistory()
   770  		de("found DeleteHistory: id:%v upto:%v", msgid, delh.Upto)
   771  		if delh.Upto == 0 {
   772  			de("skipping malformed delh")
   773  			continue
   774  		}
   775  
   776  		if expungeActive == nil || (delh.Upto > expungeActive.Upto) {
   777  			expungeActive = &chat1.Expunge{
   778  				Basis: mvalid.ServerHeader.MessageID,
   779  				Upto:  delh.Upto,
   780  			}
   781  		}
   782  	}
   783  
   784  	// Noop if there is no Expunge or DeleteHistory messages
   785  	if expungeActive == nil {
   786  		return nil, nil
   787  	}
   788  	if expungeActive.Upto == 0 {
   789  		return nil, nil
   790  	}
   791  
   792  	mem, err := s.delhTracker.getEntry(ctx, conv.GetConvID(), uid)
   793  	switch err.(type) {
   794  	case nil:
   795  		if mem.MaxDeleteHistoryUpto >= expungeActive.Upto {
   796  			// No-op if the effect has already been applied locally
   797  			de("skipping delh with no new effect: (upto local:%v >= msg:%v)", mem.MaxDeleteHistoryUpto, expungeActive.Upto)
   798  			return nil, nil
   799  		}
   800  		if expungeActive.Upto < mem.MinDeletableMessage {
   801  			// Record-only if it would delete messages earlier than the local min.
   802  			de("record-only delh: (%v < %v)", expungeActive.Upto, mem.MinDeletableMessage)
   803  			err := s.delhTracker.setMaxDeleteHistoryUpto(ctx, conv.GetConvID(), uid, expungeActive.Upto)
   804  			if err != nil {
   805  				de("failed to store delh track: %v", err)
   806  			}
   807  			return nil, nil
   808  		}
   809  		// No shortcuts, fallthrough to apply.
   810  	case MissError:
   811  		// We have no memory, assume it needs to be applied
   812  	default:
   813  		return nil, err
   814  	}
   815  
   816  	return s.applyExpunge(ctx, conv, uid, *expungeActive)
   817  }
   818  
   819  // Apply a delete history.
   820  // Returns a non-nil expunge if deletes happened.
   821  // Always runs through local messages.
   822  func (s *Storage) applyExpunge(ctx context.Context, conv types.UnboxConversationInfo,
   823  	uid gregor1.UID, expunge chat1.Expunge) (*chat1.Expunge, Error) {
   824  
   825  	convID := conv.GetConvID()
   826  	s.Debug(ctx, "applyExpunge(%v, %v, %v)", convID, uid, expunge.Upto)
   827  
   828  	de := func(format string, args ...interface{}) {
   829  		s.Debug(ctx, "applyExpunge: "+fmt.Sprintf(format, args...))
   830  	}
   831  
   832  	rc := NewInsatiableResultCollector() // collect all messages
   833  	err := s.engine.ReadMessages(ctx, rc, convID, uid, expunge.Upto-1, 0)
   834  	switch err.(type) {
   835  	case nil:
   836  		// ok
   837  	case MissError:
   838  		de("record-only delh: no local messages")
   839  		err := s.delhTracker.setMaxDeleteHistoryUpto(ctx, convID, uid, expunge.Upto)
   840  		if err != nil {
   841  			de("failed to store delh track: %v", err)
   842  		}
   843  		return nil, nil
   844  	default:
   845  		return nil, err
   846  	}
   847  
   848  	var allAssets []chat1.Asset
   849  	var writeback, allPurged []chat1.MessageUnboxed
   850  	for _, msg := range rc.Result() {
   851  		mtype := msg.GetMessageType()
   852  		if !chat1.IsDeletableByDeleteHistory(mtype) {
   853  			// Skip message types that cannot be deleted this way
   854  			continue
   855  		}
   856  		if !msg.IsValid() {
   857  			de("skipping invalid msg: %v", msg.DebugString())
   858  			continue
   859  		}
   860  		mvalid := msg.Valid()
   861  		if mvalid.MessageBody.IsNil() {
   862  			continue
   863  		}
   864  		// METADATA and HEADLINE messages are only expunged if they are not the
   865  		// latest max message.
   866  		switch mtype {
   867  		case chat1.MessageType_METADATA,
   868  			chat1.MessageType_HEADLINE:
   869  			maxMsg, err := conv.GetMaxMessage(mtype)
   870  			if err != nil {
   871  				de("delh: %v, not expunging %v", err, msg.DebugString())
   872  				continue
   873  			} else if maxMsg.MsgID == msg.GetMessageID() {
   874  				de("delh: not expunging %v, latest max message", msg.DebugString())
   875  				continue
   876  			}
   877  			de("delh: expunging %v, non-max message", msg.DebugString())
   878  		default:
   879  		}
   880  
   881  		mvalid.ServerHeader.SupersededBy = expunge.Basis // Can be 0
   882  		msgPurged, assets := s.purgeMessage(mvalid)
   883  		allPurged = append(allPurged, msg)
   884  		allAssets = append(allAssets, assets...)
   885  		writeback = append(writeback, msgPurged)
   886  	}
   887  
   888  	// queue asset deletions in the background
   889  	s.assetDeleter.DeleteAssets(ctx, uid, convID, allAssets)
   890  	// queue search index update in the background
   891  	go func(ctx context.Context) {
   892  		err := s.G().Indexer.Remove(ctx, convID, allPurged)
   893  		if err != nil {
   894  			s.Debug(ctx, "Error removing from indexer: %+v", err)
   895  		}
   896  	}(globals.BackgroundChatCtx(ctx, s.G()))
   897  
   898  	de("deleting %v messages", len(writeback))
   899  	if err = s.engine.WriteMessages(ctx, convID, uid, writeback); err != nil {
   900  		de("write messages failed: %v", err)
   901  		return nil, err
   902  	}
   903  
   904  	err = s.delhTracker.setDeletedUpto(ctx, convID, uid, expunge.Upto)
   905  	if err != nil {
   906  		de("failed to store delh track: %v", err)
   907  	}
   908  
   909  	return &expunge, nil
   910  }
   911  
   912  // clearUpthrough clears up to the given message ID, inclusive
   913  func (s *Storage) clearUpthrough(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID,
   914  	upthrough chat1.MessageID) (err Error) {
   915  	var ierr error
   916  	defer s.Trace(ctx, &ierr, "clearUpthrough")()
   917  	defer func() { ierr = s.castInternalError(err) }()
   918  	key, ierr := GetSecretBoxKey(ctx, s.G().ExternalG())
   919  	if ierr != nil {
   920  		return MiscError{Msg: "unable to get secret key: " + ierr.Error()}
   921  	}
   922  	ctx, err = s.engine.Init(ctx, key, convID, uid)
   923  	if err != nil {
   924  		return err
   925  	}
   926  
   927  	var msgIDs []chat1.MessageID
   928  	for m := upthrough; m > 0; m-- {
   929  		msgIDs = append(msgIDs, m)
   930  	}
   931  	return s.engine.ClearMessages(ctx, convID, uid, msgIDs)
   932  }
   933  
   934  // ClearBefore clears all messages up to (but not including) the upto messageID
   935  func (s *Storage) ClearBefore(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID,
   936  	upto chat1.MessageID) (err Error) {
   937  	var ierr error
   938  	defer s.Trace(ctx, &ierr, fmt.Sprintf("ClearBefore: convID: %s, uid: %s, msgID: %d", convID, uid, upto))()
   939  	defer func() { ierr = s.castInternalError(err) }()
   940  	lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), convID.String())
   941  	defer lock.Release(ctx)
   942  
   943  	// Abort, we don't want to overflow uint (chat1.MessageID)
   944  	if upto == 0 {
   945  		return nil
   946  	}
   947  	return s.clearUpthrough(ctx, convID, uid, upto-1)
   948  }
   949  
   950  func (s *Storage) ClearAll(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID) (err Error) {
   951  	var ierr error
   952  	defer s.Trace(ctx, &ierr, "ClearAll")()
   953  	defer func() { ierr = s.castInternalError(err) }()
   954  	lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), convID.String())
   955  	defer lock.Release(ctx)
   956  	maxMsgID, err := s.idtracker.getMaxMessageID(ctx, convID, uid)
   957  	if err != nil {
   958  		return err
   959  	}
   960  	return s.clearUpthrough(ctx, convID, uid, maxMsgID)
   961  }
   962  
   963  func (s *Storage) ResultCollectorFromQuery(ctx context.Context, query *chat1.GetThreadQuery,
   964  	pagination *chat1.Pagination) ResultCollector {
   965  	var num int
   966  	if pagination != nil {
   967  		num = pagination.Num
   968  	} else {
   969  		num = maxFetchNum
   970  	}
   971  
   972  	if query != nil && len(query.MessageTypes) > 0 {
   973  		s.Debug(ctx, "ResultCollectorFromQuery: types: %v", query.MessageTypes)
   974  		return NewTypedResultCollector(num, query.MessageTypes)
   975  	}
   976  	return NewSimpleResultCollector(num, false)
   977  }
   978  
   979  func (s *Storage) fetchUpToMsgIDLocked(ctx context.Context, rc ResultCollector,
   980  	convID chat1.ConversationID, uid gregor1.UID, msgID chat1.MessageID, query *chat1.GetThreadQuery,
   981  	pagination *chat1.Pagination) (res FetchResult, err Error) {
   982  
   983  	if err = isAbortedRequest(ctx); err != nil {
   984  		return res, err
   985  	}
   986  	// Fetch secret key
   987  	key, ierr := GetSecretBoxKey(ctx, s.G().ExternalG())
   988  	if ierr != nil {
   989  		return res, MiscError{Msg: "unable to get secret key: " + ierr.Error()}
   990  	}
   991  
   992  	// Init storage engine first
   993  	ctx, err = s.engine.Init(ctx, key, convID, uid)
   994  	if err != nil {
   995  		return res, s.maybeNukeLocked(ctx, false, err, convID, uid)
   996  	}
   997  
   998  	// Calculate seek parameters
   999  	var maxID, minID chat1.MessageID
  1000  	var num int
  1001  	if pagination == nil {
  1002  		maxID = msgID
  1003  		num = maxFetchNum
  1004  	} else {
  1005  		var pid chat1.MessageID
  1006  		num = pagination.Num
  1007  		if len(pagination.Next) == 0 && len(pagination.Previous) == 0 {
  1008  			maxID = msgID
  1009  		} else if len(pagination.Next) > 0 {
  1010  			if derr := decode(pagination.Next, &pid); derr != nil {
  1011  				err = RemoteError{Msg: "Fetch: failed to decode pager: " + derr.Error()}
  1012  				return res, s.maybeNukeLocked(ctx, false, err, convID, uid)
  1013  			}
  1014  			maxID = pid - 1
  1015  			minID = 0
  1016  			s.Debug(ctx, "Fetch: next pagination: pid: %d", pid)
  1017  		} else {
  1018  			if derr := decode(pagination.Previous, &pid); derr != nil {
  1019  				err = RemoteError{Msg: "Fetch: failed to decode pager: " + derr.Error()}
  1020  				return res, s.maybeNukeLocked(ctx, false, err, convID, uid)
  1021  			}
  1022  			maxID = chat1.MessageID(int(pid) + num)
  1023  			minID = pid
  1024  			s.Debug(ctx, "Fetch: prev pagination: pid: %d", pid)
  1025  		}
  1026  	}
  1027  	s.Debug(ctx, "Fetch: maxID: %d num: %d", maxID, num)
  1028  
  1029  	// Figure out how to determine we are done seeking (unless client tells us how to)
  1030  	if rc == nil {
  1031  		rc = s.ResultCollectorFromQuery(ctx, query, pagination)
  1032  	}
  1033  	s.Debug(ctx, "Fetch: using result collector: %s", rc)
  1034  
  1035  	// Run seek looking for all the messages
  1036  	if err = s.engine.ReadMessages(ctx, rc, convID, uid, maxID, minID); err != nil {
  1037  		return res, err
  1038  	}
  1039  	msgs := rc.Result()
  1040  
  1041  	// Clear out any ephemeral messages that have exploded before we hand these
  1042  	// messages out.
  1043  	explodedMsgs, err := s.explodeExpiredMessages(ctx, convID, uid, msgs)
  1044  	if err != nil {
  1045  		return res, err
  1046  	}
  1047  	res.Exploded = explodedMsgs
  1048  
  1049  	// Get the stored latest point upto which has been deleted.
  1050  	// `maxDeletedUpto` can be behind the times, so the pager is patched later in ConvSource.
  1051  	// It will be behind the times if a retention policy is the last expunger and only a full inbox sync has happened.
  1052  	var maxDeletedUpto chat1.MessageID
  1053  	delh, err := s.delhTracker.getEntry(ctx, convID, uid)
  1054  	switch err.(type) {
  1055  	case nil:
  1056  		maxDeletedUpto = delh.MaxDeleteHistoryUpto
  1057  	case MissError:
  1058  	default:
  1059  		return res, err
  1060  	}
  1061  	s.Debug(ctx, "Fetch: using max deleted upto: %v for pager", maxDeletedUpto)
  1062  
  1063  	// Form paged result
  1064  	var pmsgs []pager.Message
  1065  	for _, m := range msgs {
  1066  		pmsgs = append(pmsgs, m)
  1067  	}
  1068  	if res.Thread.Pagination, ierr = pager.NewThreadPager().MakePage(pmsgs, num, maxDeletedUpto); ierr != nil {
  1069  		return res,
  1070  			NewInternalError(ctx, s.DebugLabeler, "Fetch: failed to encode pager: %s", ierr.Error())
  1071  	}
  1072  	res.Thread.Messages = msgs
  1073  
  1074  	s.Debug(ctx, "Fetch: cache hit: num: %d", len(msgs))
  1075  	return res, nil
  1076  }
  1077  
  1078  func (s *Storage) FetchUpToLocalMaxMsgID(ctx context.Context,
  1079  	convID chat1.ConversationID, uid gregor1.UID, rc ResultCollector, iboxMaxMsgID chat1.MessageID,
  1080  	query *chat1.GetThreadQuery, pagination *chat1.Pagination) (res FetchResult, err Error) {
  1081  	var ierr error
  1082  	defer s.Trace(ctx, &ierr, "FetchUpToLocalMaxMsgID")()
  1083  	defer func() { ierr = s.castInternalError(err) }()
  1084  	lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), convID.String())
  1085  	defer lock.Release(ctx)
  1086  
  1087  	maxMsgID, err := s.idtracker.getMaxMessageID(ctx, convID, uid)
  1088  	if err != nil {
  1089  		return res, err
  1090  	}
  1091  	if iboxMaxMsgID > maxMsgID {
  1092  		s.Debug(ctx, "FetchUpToLocalMaxMsgID: overriding locally stored max msgid with ibox: %d",
  1093  			iboxMaxMsgID)
  1094  		maxMsgID = iboxMaxMsgID
  1095  	}
  1096  	s.Debug(ctx, "FetchUpToLocalMaxMsgID: using max msgID: %d", maxMsgID)
  1097  
  1098  	return s.fetchUpToMsgIDLocked(ctx, rc, convID, uid, maxMsgID, query, pagination)
  1099  }
  1100  
  1101  func (s *Storage) Fetch(ctx context.Context, conv chat1.Conversation,
  1102  	uid gregor1.UID, rc ResultCollector, query *chat1.GetThreadQuery, pagination *chat1.Pagination) (res FetchResult, err Error) {
  1103  	var ierr error
  1104  	defer s.Trace(ctx, &ierr, "Fetch")()
  1105  	defer func() { ierr = s.castInternalError(err) }()
  1106  	lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), conv.GetConvID().String())
  1107  	defer lock.Release(ctx)
  1108  
  1109  	return s.fetchUpToMsgIDLocked(ctx, rc, conv.GetConvID(), uid, conv.ReaderInfo.MaxMsgid,
  1110  		query, pagination)
  1111  }
  1112  
  1113  func (s *Storage) FetchMessages(ctx context.Context, convID chat1.ConversationID,
  1114  	uid gregor1.UID, msgIDs []chat1.MessageID) (res []*chat1.MessageUnboxed, err Error) {
  1115  	var ierr error
  1116  	defer s.Trace(ctx, &ierr, "FetchMessages")()
  1117  	defer func() { ierr = s.castInternalError(err) }()
  1118  	lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), convID.String())
  1119  	defer lock.Release(ctx)
  1120  	if err = isAbortedRequest(ctx); err != nil {
  1121  		return res, err
  1122  	}
  1123  	// Fetch secret key
  1124  	key, ierr := GetSecretBoxKey(ctx, s.G().ExternalG())
  1125  	if ierr != nil {
  1126  		return nil, MiscError{Msg: "unable to get secret key: " + ierr.Error()}
  1127  	}
  1128  
  1129  	// Init storage engine first
  1130  	ctx, err = s.engine.Init(ctx, key, convID, uid)
  1131  	if err != nil {
  1132  		return nil, s.maybeNukeLocked(ctx, false, err, convID, uid)
  1133  	}
  1134  
  1135  	// Run seek looking for each message
  1136  	for _, msgID := range msgIDs {
  1137  		if msgID == 0 {
  1138  			res = append(res, nil)
  1139  			continue
  1140  		}
  1141  		msg, err := s.getMessage(ctx, convID, uid, msgID)
  1142  		if err != nil {
  1143  			return nil, s.maybeNukeLocked(ctx, false, err, convID, uid)
  1144  		}
  1145  		res = append(res, msg)
  1146  	}
  1147  	var msgs []chat1.MessageUnboxed
  1148  	// msgID -> index in res
  1149  	msgMap := make(map[chat1.MessageID]int)
  1150  	for i, m := range res {
  1151  		if m != nil {
  1152  			msg := *m
  1153  			msgs = append(msgs, msg)
  1154  			msgMap[msg.GetMessageID()] = i
  1155  		}
  1156  	}
  1157  
  1158  	_, err = s.explodeExpiredMessages(ctx, convID, uid, msgs)
  1159  	if err != nil {
  1160  		return nil, err
  1161  	}
  1162  	// write back any purged messages into our result.
  1163  	for _, m := range msgs {
  1164  		index, ok := msgMap[m.GetMessageID()]
  1165  		if !ok {
  1166  			s.Debug(ctx, "unable to find msg %d in msgMap", m.GetMessageID())
  1167  			continue
  1168  		}
  1169  		msg := m
  1170  		res[index] = &msg
  1171  	}
  1172  
  1173  	return res, nil
  1174  }
  1175  
  1176  func (s *Storage) FetchUnreadlineID(ctx context.Context, convID chat1.ConversationID,
  1177  	uid gregor1.UID, readMsgID chat1.MessageID) (msgID *chat1.MessageID, err Error) {
  1178  	var ierr error
  1179  	defer s.Trace(ctx, &ierr, "FetchUnreadlineID")()
  1180  	defer func() { ierr = s.castInternalError(err) }()
  1181  	lock := locks.StorageLockTab.AcquireOnName(ctx, s.G(), convID.String())
  1182  	defer lock.Release(ctx)
  1183  	if err = isAbortedRequest(ctx); err != nil {
  1184  		return nil, err
  1185  	}
  1186  	// Fetch secret key
  1187  	key, ierr := GetSecretBoxKey(ctx, s.G().ExternalG())
  1188  	if ierr != nil {
  1189  		return nil, MiscError{Msg: "unable to get secret key: " + ierr.Error()}
  1190  	}
  1191  
  1192  	// Init storage engine first
  1193  	ctx, err = s.engine.Init(ctx, key, convID, uid)
  1194  	if err != nil {
  1195  		return nil, s.maybeNukeLocked(ctx, false, err, convID, uid)
  1196  	}
  1197  
  1198  	// Run seek looking for each message
  1199  	for unreadlineID := readMsgID + 1; unreadlineID < readMsgID+1000; unreadlineID++ {
  1200  		msg, err := s.getMessage(ctx, convID, uid, unreadlineID)
  1201  		if err != nil {
  1202  			return nil, s.maybeNukeLocked(ctx, false, err, convID, uid)
  1203  		}
  1204  		// If we are missing any messages just abort.
  1205  		if msg == nil {
  1206  			return nil, nil
  1207  		}
  1208  		// return the first non-deleted visible message we have
  1209  		if msg.IsValidFull() && utils.IsVisibleChatMessageType(msg.GetMessageType()) {
  1210  			return &unreadlineID, nil
  1211  		}
  1212  	}
  1213  
  1214  	return nil, nil
  1215  }
  1216  
  1217  func (s *Storage) UpdateTLFIdentifyBreak(ctx context.Context, tlfID chat1.TLFID,
  1218  	breaks []keybase1.TLFIdentifyFailure) error {
  1219  	return s.breakTracker.UpdateTLF(ctx, tlfID, breaks)
  1220  }
  1221  
  1222  func (s *Storage) IsTLFIdentifyBroken(ctx context.Context, tlfID chat1.TLFID) bool {
  1223  	idBroken, err := s.breakTracker.IsTLFBroken(ctx, tlfID)
  1224  	if err != nil {
  1225  		s.Debug(ctx, "IsTLFIdentifyBroken: got error, so returning broken: %s", err.Error())
  1226  		return true
  1227  	}
  1228  	return idBroken
  1229  }
  1230  
  1231  func (s *Storage) getMessage(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID, msgID chat1.MessageID) (*chat1.MessageUnboxed, Error) {
  1232  	rc := NewSimpleResultCollector(1, true)
  1233  	if err := s.engine.ReadMessages(ctx, rc, convID, uid, msgID, 0); err != nil {
  1234  		// If we don't have the message, just keep going
  1235  		if _, ok := err.(MissError); ok {
  1236  			return nil, nil
  1237  		}
  1238  		return nil, err
  1239  	}
  1240  	res := rc.Result()
  1241  	if len(res) == 0 {
  1242  		return nil, nil
  1243  	}
  1244  	return &res[0], nil
  1245  }
  1246  
  1247  func (s *Storage) updateUnfurlTargetOnDelete(ctx context.Context, convID chat1.ConversationID,
  1248  	uid gregor1.UID, unfurlMsg chat1.MessageUnboxed) (res chat1.MessageUnboxed, err error) {
  1249  	defer s.Trace(ctx, &err, "updateUnfurlTargetOnDelete(%d)",
  1250  		unfurlMsg.GetMessageID())()
  1251  	if unfurlMsg.Valid().MessageBody.IsNil() {
  1252  		return unfurlMsg, errors.New("unfurl already deleted")
  1253  	}
  1254  	targetMsgID := unfurlMsg.Valid().MessageBody.Unfurl().MessageID
  1255  	targetMsg, err := s.getMessage(ctx, convID, uid, targetMsgID)
  1256  	if err != nil || targetMsg == nil {
  1257  		s.Debug(ctx, "updateUnfurlTargetOnDelete: no target message found: err: %s", err)
  1258  		return unfurlMsg, err
  1259  	}
  1260  	if !targetMsg.IsValid() {
  1261  		s.Debug(ctx, "updateUnfurlTargetOnDelete: target message is unvalid")
  1262  		return unfurlMsg, nil
  1263  	}
  1264  	mvalid := targetMsg.Valid()
  1265  	utils.RemoveUnfurl(&mvalid, unfurlMsg.GetMessageID())
  1266  	return chat1.NewMessageUnboxedWithValid(mvalid), nil
  1267  }
  1268  
  1269  func (s *Storage) updateRepliesAffected(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID,
  1270  	replies []chat1.MessageID, replyMap map[chat1.MessageID]chat1.MessageUnboxed) {
  1271  	if len(replies) == 0 {
  1272  		return
  1273  	}
  1274  	defer s.Trace(ctx, nil, "updateRepliesAffected: num: %d", len(replies))()
  1275  	for _, reply := range replies {
  1276  		if _, ok := replyMap[reply]; ok {
  1277  			continue
  1278  		}
  1279  		replyMsg, err := s.getMessage(ctx, convID, uid, reply)
  1280  		if err != nil || replyMsg == nil {
  1281  			s.Debug(ctx, "updateRepliesAffected: failed to get message: err: %s", err)
  1282  			continue
  1283  		}
  1284  		replyMap[reply] = *replyMsg
  1285  	}
  1286  }
  1287  
  1288  func (s *Storage) GetExplodedReplies(ctx context.Context, convID chat1.ConversationID, uid gregor1.UID,
  1289  	exploded []chat1.MessageUnboxed) []chat1.MessageUnboxed {
  1290  	if len(exploded) == 0 {
  1291  		return nil
  1292  	}
  1293  	defer s.Trace(ctx, nil, "getExplodedReplies: num: %d", len(exploded))()
  1294  	var replies []chat1.MessageID
  1295  	for _, msg := range exploded {
  1296  		if !msg.IsValid() {
  1297  			continue
  1298  		}
  1299  		replies = append(replies, msg.Valid().ServerHeader.Replies...)
  1300  	}
  1301  	replyMap := make(map[chat1.MessageID]chat1.MessageUnboxed)
  1302  	s.updateRepliesAffected(ctx, convID, uid, replies, replyMap)
  1303  	return s.flatten(replyMap)
  1304  }
  1305  
  1306  // updateReactionIDs appends `msgid` to `reactionIDs` if it is not already
  1307  // present.
  1308  func (s *Storage) updateReactionIDs(reactionIDs []chat1.MessageID, msgid chat1.MessageID) ([]chat1.MessageID, bool) {
  1309  	for _, reactionID := range reactionIDs {
  1310  		if reactionID == msgid {
  1311  			return reactionIDs, false
  1312  		}
  1313  	}
  1314  	return append(reactionIDs, msgid), true
  1315  }
  1316  
  1317  // updateReactionTargetOnDelete modifies the reaction's target message when the
  1318  // reaction itself is deleted
  1319  func (s *Storage) updateReactionTargetOnDelete(ctx context.Context, convID chat1.ConversationID,
  1320  	uid gregor1.UID, reactionMsg *chat1.MessageUnboxed) (*chat1.MessageUnboxed, bool, Error) {
  1321  	s.Debug(ctx, "updateReactionTargetOnDelete: reationMsg: %v", reactionMsg)
  1322  
  1323  	if reactionMsg.Valid().MessageBody.IsNil() {
  1324  		return nil, false, nil
  1325  	}
  1326  
  1327  	targetMsgID := reactionMsg.Valid().MessageBody.Reaction().MessageID
  1328  	targetMsg, err := s.getMessage(ctx, convID, uid, targetMsgID)
  1329  	if err != nil || targetMsg == nil {
  1330  		return nil, false, err
  1331  	}
  1332  	if targetMsg.IsValid() {
  1333  		mvalid := targetMsg.Valid()
  1334  		reactionIDs := []chat1.MessageID{}
  1335  		for _, msgID := range mvalid.ServerHeader.ReactionIDs {
  1336  			if msgID != reactionMsg.GetMessageID() {
  1337  				reactionIDs = append(reactionIDs, msgID)
  1338  			}
  1339  		}
  1340  		updated := len(mvalid.ServerHeader.ReactionIDs) != len(reactionIDs)
  1341  		mvalid.ServerHeader.ReactionIDs = reactionIDs
  1342  		newMsg := chat1.NewMessageUnboxedWithValid(mvalid)
  1343  		return &newMsg, updated, nil
  1344  	}
  1345  	return nil, false, nil
  1346  }
  1347  
  1348  // Clears the body of a message and returns any assets to be deleted.
  1349  func (s *Storage) purgeMessage(mvalid chat1.MessageUnboxedValid) (chat1.MessageUnboxed, []chat1.Asset) {
  1350  	assets := utils.AssetsForMessage(s.G(), mvalid.MessageBody)
  1351  	var emptyBody chat1.MessageBody
  1352  	mvalid.MessageBody = emptyBody
  1353  	var emptyReactions chat1.ReactionMap
  1354  	mvalid.Reactions = emptyReactions
  1355  	return chat1.NewMessageUnboxedWithValid(mvalid), assets
  1356  }