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

     1  package storage
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"fmt"
     7  	"sync"
     8  	"time"
     9  
    10  	"sort"
    11  
    12  	"github.com/keybase/client/go/chat/globals"
    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  	"github.com/keybase/clockwork"
    19  )
    20  
    21  type outboxStorage interface {
    22  	readStorage(ctx context.Context) (diskOutbox, Error)
    23  	writeStorage(ctx context.Context, do diskOutbox) Error
    24  	name() string
    25  }
    26  
    27  type OutboxPendingPreviewFn func(context.Context, *chat1.OutboxRecord) error
    28  type OutboxNewMessageNotifierFn func(context.Context, chat1.OutboxRecord)
    29  
    30  type Outbox struct {
    31  	globals.Contextified
    32  	utils.DebugLabeler
    33  	outboxStorage
    34  
    35  	clock              clockwork.Clock
    36  	uid                gregor1.UID
    37  	pendingPreviewer   OutboxPendingPreviewFn
    38  	newMessageNotifier OutboxNewMessageNotifierFn
    39  }
    40  
    41  const outboxVersion = 4
    42  const ephemeralPurgeCutoff = 24 * time.Hour
    43  const errorPurgeCutoff = time.Hour * 24 * 7 // one week
    44  
    45  // Ordinals for the outbox start at 100.
    46  // So that journeycard ordinals, which are added at the last minute by postProcessConv, do not conflict.
    47  const outboxOrdinalStart = 100
    48  
    49  type diskOutbox struct {
    50  	Version int                  `codec:"V"`
    51  	Records []chat1.OutboxRecord `codec:"O"`
    52  }
    53  
    54  func (d diskOutbox) DeepCopy() diskOutbox {
    55  	obrs := make([]chat1.OutboxRecord, 0, len(d.Records))
    56  	for _, obr := range d.Records {
    57  		obrs = append(obrs, obr.DeepCopy())
    58  	}
    59  	return diskOutbox{
    60  		Version: d.Version,
    61  		Records: obrs,
    62  	}
    63  }
    64  
    65  func NewOutboxID() (chat1.OutboxID, error) {
    66  	rbs, err := libkb.RandBytes(8)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	return chat1.OutboxID(rbs), nil
    71  }
    72  
    73  func DeriveOutboxID(dat []byte) chat1.OutboxID {
    74  	h := sha256.Sum256(dat)
    75  	return chat1.OutboxID(h[:8])
    76  }
    77  
    78  func GetOutboxIDFromURL(url string, convID chat1.ConversationID, msg chat1.MessageUnboxed) chat1.OutboxID {
    79  	seed := fmt.Sprintf("%s:%s:%d", url, convID, msg.GetMessageID())
    80  	return DeriveOutboxID([]byte(seed))
    81  }
    82  
    83  var storageReportOnce sync.Once
    84  
    85  func PendingPreviewer(p OutboxPendingPreviewFn) func(*Outbox) {
    86  	return func(o *Outbox) {
    87  		o.SetPendingPreviewer(p)
    88  	}
    89  }
    90  
    91  func NewMessageNotifier(n OutboxNewMessageNotifierFn) func(*Outbox) {
    92  	return func(o *Outbox) {
    93  		o.SetNewMessageNotifier(n)
    94  	}
    95  }
    96  
    97  func NewOutbox(g *globals.Context, uid gregor1.UID, config ...func(*Outbox)) *Outbox {
    98  	st := newOutboxBaseboxStorage(g, uid)
    99  	o := &Outbox{
   100  		Contextified:  globals.NewContextified(g),
   101  		DebugLabeler:  utils.NewDebugLabeler(g.ExternalG(), "Outbox", false),
   102  		outboxStorage: st,
   103  		uid:           uid,
   104  		clock:         clockwork.NewRealClock(),
   105  	}
   106  	for _, c := range config {
   107  		c(o)
   108  	}
   109  	storageReportOnce.Do(func() {
   110  		o.Debug(context.Background(), "NewOutbox: using storage engine: %s", st.name())
   111  	})
   112  	return o
   113  }
   114  
   115  func (o *Outbox) SetPendingPreviewer(p OutboxPendingPreviewFn) {
   116  	o.pendingPreviewer = p
   117  }
   118  
   119  func (o *Outbox) SetNewMessageNotifier(n OutboxNewMessageNotifierFn) {
   120  	o.newMessageNotifier = n
   121  }
   122  
   123  func (o *Outbox) GetUID() gregor1.UID {
   124  	return o.uid
   125  }
   126  
   127  type ByCtimeOrder []chat1.OutboxRecord
   128  
   129  func (a ByCtimeOrder) Len() int      { return len(a) }
   130  func (a ByCtimeOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   131  func (a ByCtimeOrder) Less(i, j int) bool {
   132  	return a[i].Ctime.Before(a[j].Ctime)
   133  }
   134  
   135  func (o *Outbox) SetClock(cl clockwork.Clock) {
   136  	o.clock = cl
   137  }
   138  
   139  func (o *Outbox) PushMessage(ctx context.Context, convID chat1.ConversationID,
   140  	msg chat1.MessagePlaintext, suppliedOutboxID *chat1.OutboxID,
   141  	sendOpts *chat1.SenderSendOptions, prepareOpts *chat1.SenderPrepareOptions,
   142  	identifyBehavior keybase1.TLFIdentifyBehavior) (rec chat1.OutboxRecord, err Error) {
   143  	locks.Outbox.Lock()
   144  	defer locks.Outbox.Unlock()
   145  
   146  	// Read outbox for the user
   147  	obox, err := o.readStorage(ctx)
   148  	if err != nil {
   149  		if _, ok := err.(MissError); !ok {
   150  			return rec, err
   151  		}
   152  		obox = diskOutbox{
   153  			Version: outboxVersion,
   154  			Records: []chat1.OutboxRecord{},
   155  		}
   156  	}
   157  
   158  	// Generate new outbox ID (unless the caller supplied it for us already)
   159  	var outboxID chat1.OutboxID
   160  	if suppliedOutboxID == nil {
   161  		var ierr error
   162  		outboxID, ierr = NewOutboxID()
   163  		if ierr != nil {
   164  			return rec, NewInternalError(ctx, o.DebugLabeler, "error getting outboxID: err: %s", ierr)
   165  		}
   166  	} else {
   167  		outboxID = *suppliedOutboxID
   168  	}
   169  
   170  	// Compute prev ordinal by predicting that all outbox messages will be appended to the thread
   171  	prevOrdinal := outboxOrdinalStart
   172  	for _, obr := range obox.Records {
   173  		if obr.ConvID.Eq(convID) && obr.Ordinal >= outboxOrdinalStart && obr.Ordinal >= prevOrdinal {
   174  			prevOrdinal = obr.Ordinal + 1
   175  		}
   176  	}
   177  
   178  	// Append record
   179  	msg.ClientHeader.OutboxID = &outboxID
   180  	rec = chat1.OutboxRecord{
   181  		State:            chat1.NewOutboxStateWithSending(0),
   182  		Msg:              msg,
   183  		Ctime:            gregor1.ToTime(o.clock.Now()),
   184  		ConvID:           convID,
   185  		OutboxID:         outboxID,
   186  		IdentifyBehavior: identifyBehavior,
   187  		Ordinal:          prevOrdinal,
   188  		SendOpts:         sendOpts,
   189  		PrepareOpts:      prepareOpts,
   190  	}
   191  	obox.Records = append(obox.Records, rec)
   192  
   193  	// Add any pending attachment previews for the notification and return value
   194  	if o.pendingPreviewer != nil {
   195  		if err := o.pendingPreviewer(ctx, &rec); err != nil {
   196  			o.Debug(ctx, "PushMessage: failed to add pending preview: %v", err)
   197  		}
   198  	}
   199  	// Run the notification before we write to the disk so that it is guaranteed to beat
   200  	// any notifications from the message being sent
   201  	if o.newMessageNotifier != nil {
   202  		o.newMessageNotifier(ctx, rec)
   203  	}
   204  
   205  	// Write out diskbox
   206  	obox.Version = outboxVersion
   207  	if err = o.writeStorage(ctx, obox); err != nil {
   208  		return rec, err
   209  	}
   210  
   211  	return rec, nil
   212  }
   213  
   214  // PullAllConversations grabs all outbox entries for the current outbox, and optionally deletes them
   215  // from storage
   216  func (o *Outbox) PullAllConversations(ctx context.Context, includeErrors bool, remove bool) ([]chat1.OutboxRecord, error) {
   217  	locks.Outbox.Lock()
   218  	defer locks.Outbox.Unlock()
   219  
   220  	// Read outbox for the user
   221  	obox, err := o.readStorage(ctx)
   222  	if err != nil {
   223  		return nil, err
   224  	}
   225  
   226  	var res, errors []chat1.OutboxRecord
   227  	for _, obr := range obox.Records {
   228  		state, err := obr.State.State()
   229  		if err != nil {
   230  			o.Debug(ctx, "PullAllConversations: unknown state item: skipping: err: %v", err)
   231  			continue
   232  		}
   233  		if state == chat1.OutboxStateType_ERROR {
   234  			if includeErrors {
   235  				res = append(res, obr)
   236  			} else {
   237  				errors = append(errors, obr)
   238  			}
   239  		} else {
   240  			res = append(res, obr)
   241  		}
   242  	}
   243  	if remove {
   244  		// Write out diskbox
   245  		obox.Records = errors
   246  		obox.Version = outboxVersion
   247  		if err := o.writeStorage(ctx, obox); err != nil {
   248  			return nil, err
   249  		}
   250  	}
   251  
   252  	return res, nil
   253  }
   254  
   255  // RecordFailedAttempt will either modify an existing matching record (if sending) to next attempt
   256  // number, or if the record doesn't exist it adds it in.
   257  func (o *Outbox) RecordFailedAttempt(ctx context.Context, oldObr chat1.OutboxRecord) error {
   258  	locks.Outbox.Lock()
   259  	defer locks.Outbox.Unlock()
   260  
   261  	// Read outbox for the user
   262  	obox, err := o.readStorage(ctx)
   263  	if err != nil {
   264  		if _, ok := err.(MissError); !ok {
   265  			return err
   266  		}
   267  		obox = diskOutbox{
   268  			Version: outboxVersion,
   269  			Records: []chat1.OutboxRecord{},
   270  		}
   271  	}
   272  
   273  	// Loop through what we have and make sure we don't already have this record in here
   274  	var recs []chat1.OutboxRecord
   275  	added := false
   276  	for _, obr := range obox.Records {
   277  		if obr.OutboxID.Eq(&oldObr.OutboxID) {
   278  			state, err := obr.State.State()
   279  			if err != nil {
   280  				return err
   281  			}
   282  			if state == chat1.OutboxStateType_SENDING {
   283  				obr.State = chat1.NewOutboxStateWithSending(obr.State.Sending() + 1)
   284  			}
   285  			added = true
   286  		}
   287  		recs = append(recs, obr)
   288  	}
   289  	if !added {
   290  		state, err := oldObr.State.State()
   291  		if err != nil {
   292  			return err
   293  		}
   294  		if state == chat1.OutboxStateType_SENDING {
   295  			oldObr.State = chat1.NewOutboxStateWithSending(oldObr.State.Sending() + 1)
   296  		}
   297  		recs = append(recs, oldObr)
   298  		sort.Sort(ByCtimeOrder(recs))
   299  	}
   300  
   301  	// Write out diskbox
   302  	obox.Records = recs
   303  	if err := o.writeStorage(ctx, obox); err != nil {
   304  		return err
   305  	}
   306  	return nil
   307  }
   308  
   309  func (o *Outbox) MarkConvAsError(ctx context.Context, convID chat1.ConversationID,
   310  	errRec chat1.OutboxStateError) (res []chat1.OutboxRecord, err error) {
   311  	locks.Outbox.Lock()
   312  	defer locks.Outbox.Unlock()
   313  	obox, err := o.readStorage(ctx)
   314  	if err != nil {
   315  		return res, err
   316  	}
   317  	var recs []chat1.OutboxRecord
   318  	for _, iobr := range obox.Records {
   319  		state, err := iobr.State.State()
   320  		if err != nil {
   321  			o.Debug(ctx, "MarkAllAsError: unknown state item: adding: err: %s", err.Error())
   322  			recs = append(recs, iobr)
   323  			continue
   324  		}
   325  		if iobr.ConvID.Eq(convID) && state != chat1.OutboxStateType_ERROR {
   326  			iobr.State = chat1.NewOutboxStateWithError(errRec)
   327  			res = append(res, iobr)
   328  		}
   329  		recs = append(recs, iobr)
   330  	}
   331  	obox.Records = recs
   332  	if err := o.writeStorage(ctx, obox); err != nil {
   333  		return res, err
   334  	}
   335  	return res, nil
   336  }
   337  
   338  // MarkAsError will either mark an existing record as an error, or it will add the passed
   339  // record as an error with the specified error state
   340  func (o *Outbox) MarkAsError(ctx context.Context, obr chat1.OutboxRecord, errRec chat1.OutboxStateError) (res chat1.OutboxRecord, err error) {
   341  	locks.Outbox.Lock()
   342  	defer locks.Outbox.Unlock()
   343  
   344  	// Read outbox for the user
   345  	obox, err := o.readStorage(ctx)
   346  	if err != nil {
   347  		return res, err
   348  	}
   349  
   350  	// Loop through and find record
   351  	var recs []chat1.OutboxRecord
   352  	added := false
   353  	for _, iobr := range obox.Records {
   354  		if iobr.OutboxID.Eq(&obr.OutboxID) {
   355  			iobr.State = chat1.NewOutboxStateWithError(errRec)
   356  			added = true
   357  			res = iobr
   358  		}
   359  		recs = append(recs, iobr)
   360  	}
   361  	if !added {
   362  		obr.State = chat1.NewOutboxStateWithError(errRec)
   363  		res = obr
   364  		recs = append(recs, obr)
   365  		sort.Sort(ByCtimeOrder(recs))
   366  	}
   367  
   368  	// Write out diskbox
   369  	obox.Records = recs
   370  	if err := o.writeStorage(ctx, obox); err != nil {
   371  		return res, err
   372  	}
   373  	return res, nil
   374  }
   375  
   376  func (o *Outbox) RetryMessage(ctx context.Context, obid chat1.OutboxID,
   377  	identifyBehavior *keybase1.TLFIdentifyBehavior) (res *chat1.OutboxRecord, err error) {
   378  	locks.Outbox.Lock()
   379  	defer locks.Outbox.Unlock()
   380  
   381  	// Read outbox for the user
   382  	obox, err := o.readStorage(ctx)
   383  	if err != nil {
   384  		return res, err
   385  	}
   386  
   387  	// Loop through and find record
   388  	var recs []chat1.OutboxRecord
   389  	for _, obr := range obox.Records {
   390  		if obr.OutboxID.Eq(&obid) {
   391  			o.Debug(ctx, "resetting send information on obid: %s", obid)
   392  			obr.State = chat1.NewOutboxStateWithSending(0)
   393  			obr.Ctime = gregor1.ToTime(o.clock.Now())
   394  			if identifyBehavior != nil {
   395  				obr.IdentifyBehavior = *identifyBehavior
   396  			}
   397  			res = &obr
   398  		}
   399  		recs = append(recs, obr)
   400  	}
   401  
   402  	// Write out diskbox
   403  	obox.Records = recs
   404  	if err := o.writeStorage(ctx, obox); err != nil {
   405  		return res, err
   406  	}
   407  	return res, nil
   408  }
   409  
   410  func (o *Outbox) GetRecord(ctx context.Context, outboxID chat1.OutboxID) (res chat1.OutboxRecord, err error) {
   411  	locks.Outbox.Lock()
   412  	defer locks.Outbox.Unlock()
   413  	obox, err := o.readStorage(ctx)
   414  	if err != nil {
   415  		return res, err
   416  	}
   417  	for _, obr := range obox.Records {
   418  		if obr.OutboxID.Eq(&outboxID) {
   419  			return obr, nil
   420  		}
   421  	}
   422  	return res, MissError{}
   423  }
   424  
   425  func (o *Outbox) UpdateMessage(ctx context.Context, replaceobr chat1.OutboxRecord) (updated bool, err error) {
   426  	locks.Outbox.Lock()
   427  	defer locks.Outbox.Unlock()
   428  	obox, err := o.readStorage(ctx)
   429  	if err != nil {
   430  		return false, err
   431  	}
   432  	// Scan to find the message and replace it
   433  	var recs []chat1.OutboxRecord
   434  	for _, obr := range obox.Records {
   435  		if !obr.OutboxID.Eq(&replaceobr.OutboxID) {
   436  			recs = append(recs, obr)
   437  		} else {
   438  			updated = true
   439  			recs = append(recs, replaceobr)
   440  		}
   441  	}
   442  	obox.Records = recs
   443  	if err := o.writeStorage(ctx, obox); err != nil {
   444  		return false, err
   445  	}
   446  	return updated, nil
   447  }
   448  
   449  func (o *Outbox) CancelMessagesWithPredicate(ctx context.Context, shouldCancel func(chat1.OutboxRecord) bool) (int, error) {
   450  	locks.Outbox.Lock()
   451  	defer locks.Outbox.Unlock()
   452  
   453  	// Read outbox for the user
   454  	obox, err := o.readStorage(ctx)
   455  	if err != nil {
   456  		if _, ok := err.(MissError); !ok {
   457  			return 0, err
   458  		}
   459  	}
   460  
   461  	// Remove any records that match the predicate
   462  	var recs []chat1.OutboxRecord
   463  	numCancelled := 0
   464  	for _, obr := range obox.Records {
   465  		if shouldCancel(obr) {
   466  			o.cleanupOutboxItem(ctx, obr)
   467  			numCancelled++
   468  		} else {
   469  			recs = append(recs, obr)
   470  		}
   471  	}
   472  	obox.Records = recs
   473  
   474  	// Write out box
   475  	if err := o.writeStorage(ctx, obox); err != nil {
   476  		return 0, err
   477  	}
   478  	return numCancelled, nil
   479  }
   480  
   481  func (o *Outbox) RemoveMessage(ctx context.Context, obid chat1.OutboxID) (res chat1.OutboxRecord, err error) {
   482  	locks.Outbox.Lock()
   483  	defer locks.Outbox.Unlock()
   484  
   485  	// Read outbox for the user
   486  	obox, err := o.readStorage(ctx)
   487  	if err != nil {
   488  		return res, err
   489  	}
   490  
   491  	// Scan to find the message and don't include it
   492  	var recs []chat1.OutboxRecord
   493  	for _, obr := range obox.Records {
   494  		if obr.OutboxID.Eq(&obid) {
   495  			res = obr
   496  			o.cleanupOutboxItem(ctx, obr)
   497  			continue
   498  		}
   499  		recs = append(recs, obr)
   500  	}
   501  	obox.Records = recs
   502  
   503  	// Write out box
   504  	return res, o.writeStorage(ctx, obox)
   505  }
   506  
   507  func (o *Outbox) AppendToThread(ctx context.Context, convID chat1.ConversationID,
   508  	thread *chat1.ThreadView) error {
   509  	locks.Outbox.Lock()
   510  	defer locks.Outbox.Unlock()
   511  
   512  	// Read outbox for the user
   513  	obox, err := o.readStorage(ctx)
   514  	if err != nil {
   515  		return err
   516  	}
   517  
   518  	// Sprinkle each outbox message in once
   519  	threadOutboxIDs := make(map[string]bool)
   520  	for _, m := range thread.Messages {
   521  		outboxID := m.GetOutboxID()
   522  		if outboxID != nil {
   523  			threadOutboxIDs[outboxID.String()] = true
   524  		}
   525  	}
   526  
   527  	for _, obr := range obox.Records {
   528  		// skip outbox records that are not able to be retried.
   529  		if !(obr.ConvID.Eq(convID) && obr.Msg.IsBadgableType()) {
   530  			continue
   531  		}
   532  		if threadOutboxIDs[obr.OutboxID.String()] {
   533  			o.Debug(ctx, "skipping outbox item already in the thread: %s", obr.OutboxID)
   534  			continue
   535  		}
   536  		st, err := obr.State.State()
   537  		if err != nil {
   538  			continue
   539  		}
   540  		if st == chat1.OutboxStateType_ERROR && obr.State.Error().Typ == chat1.OutboxErrorType_DUPLICATE {
   541  			o.Debug(ctx, "skipping sprinkle on duplicate message error: %s", obr.OutboxID)
   542  			continue
   543  		}
   544  		thread.Messages = append([]chat1.MessageUnboxed{chat1.NewMessageUnboxedWithOutbox(obr)},
   545  			thread.Messages...)
   546  	}
   547  	// Update prev values for outbox messages to point at correct place (in case it has changed since
   548  	// some messages got sent)
   549  	for index := len(thread.Messages) - 2; index >= 0; index-- {
   550  		msg := thread.Messages[index]
   551  		typ, err := msg.State()
   552  		if err != nil {
   553  			continue
   554  		}
   555  		if typ == chat1.MessageUnboxedState_OUTBOX {
   556  			obr := msg.Outbox()
   557  			obr.Msg.ClientHeader.OutboxInfo.Prev = thread.Messages[index+1].GetMessageID()
   558  			thread.Messages[index] = chat1.NewMessageUnboxedWithOutbox(obr)
   559  		}
   560  	}
   561  
   562  	return nil
   563  }
   564  
   565  // OutboxPurge is called periodically to ensure messages don't hang out too
   566  // long in the outbox (since they are not encrypted with ephemeral keys until
   567  // they leave it). Currently we purge anything that is in the error state and
   568  // has been in the outbox for > errorPurgeCutoff minutes for regular messages
   569  // or ephemeralPurgeCutoff minutes for ephemeral messages.
   570  func (o *Outbox) OutboxPurge(ctx context.Context) (ephemeralPurged []chat1.OutboxRecord, err error) {
   571  	locks.Outbox.Lock()
   572  	defer locks.Outbox.Unlock()
   573  
   574  	// Read outbox for the user
   575  	obox, err := o.readStorage(ctx)
   576  	if err != nil {
   577  		return nil, err
   578  	}
   579  
   580  	var recs []chat1.OutboxRecord
   581  	for _, obr := range obox.Records {
   582  		st, err := obr.State.State()
   583  		if err != nil {
   584  			o.Debug(ctx, "purging message from outbox with error getting state: %v", err)
   585  			o.cleanupOutboxItem(ctx, obr)
   586  			continue
   587  		}
   588  		if st == chat1.OutboxStateType_ERROR {
   589  			if obr.Msg.IsEphemeral() && obr.Ctime.Time().Add(ephemeralPurgeCutoff).Before(o.clock.Now()) {
   590  				o.Debug(ctx, "purging ephemeral message from outbox with error state that was older than %v: %s",
   591  					ephemeralPurgeCutoff, obr.OutboxID)
   592  				o.cleanupOutboxItem(ctx, obr)
   593  				ephemeralPurged = append(ephemeralPurged, obr)
   594  				continue
   595  			}
   596  
   597  			if !obr.Msg.IsEphemeral() && obr.Ctime.Time().Add(errorPurgeCutoff).Before(o.clock.Now()) {
   598  				o.Debug(ctx, "purging message from outbox with error state that was older than %v: %s",
   599  					errorPurgeCutoff, obr.OutboxID)
   600  				o.cleanupOutboxItem(ctx, obr)
   601  				continue
   602  			}
   603  		}
   604  		recs = append(recs, obr)
   605  	}
   606  
   607  	obox.Records = recs
   608  
   609  	// Write out diskbox
   610  	if err := o.writeStorage(ctx, obox); err != nil {
   611  		return nil, err
   612  	}
   613  	return ephemeralPurged, nil
   614  }
   615  
   616  // cleanupOutboxItem clears any external stores when an outbox item is deleted.
   617  // Currently this includes:
   618  //   - upload tasks/temp files/pending previews
   619  //   - unfurls
   620  func (o *Outbox) cleanupOutboxItem(ctx context.Context, obr chat1.OutboxRecord) {
   621  	o.G().AttachmentUploader.Complete(ctx, obr.OutboxID)
   622  	o.G().Unfurler.Complete(ctx, obr.OutboxID)
   623  }
   624  
   625  func (o *Outbox) PullForConversation(ctx context.Context, convID chat1.ConversationID) ([]chat1.OutboxRecord, error) {
   626  	locks.Outbox.Lock()
   627  	defer locks.Outbox.Unlock()
   628  
   629  	// Read outbox for the user
   630  	obox, err := o.readStorage(ctx)
   631  	if err != nil {
   632  		return nil, err
   633  	}
   634  
   635  	var recs []chat1.OutboxRecord
   636  	for _, obr := range obox.Records {
   637  		if !obr.ConvID.Eq(convID) {
   638  			continue
   639  		}
   640  		recs = append(recs, obr)
   641  	}
   642  	return recs, nil
   643  }