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

     1  package chat
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/keybase/client/go/chat/bots"
    12  	"github.com/keybase/client/go/chat/globals"
    13  	"github.com/keybase/client/go/chat/storage"
    14  	"github.com/keybase/client/go/chat/types"
    15  	"github.com/keybase/client/go/chat/utils"
    16  	"github.com/keybase/client/go/libkb"
    17  	"github.com/keybase/client/go/protocol/chat1"
    18  	"github.com/keybase/client/go/protocol/gregor1"
    19  	"github.com/keybase/client/go/protocol/keybase1"
    20  	"golang.org/x/sync/errgroup"
    21  )
    22  
    23  type conversationLocalizer interface {
    24  	Localize(ctx context.Context, uid gregor1.UID, inbox types.Inbox, maxLocalize *int) ([]chat1.ConversationLocal, error)
    25  	Name() string
    26  }
    27  
    28  type baseLocalizer struct {
    29  	globals.Contextified
    30  	utils.DebugLabeler
    31  	pipeline *localizerPipeline
    32  }
    33  
    34  func newBaseLocalizer(g *globals.Context, pipeline *localizerPipeline) *baseLocalizer {
    35  	return &baseLocalizer{
    36  		Contextified: globals.NewContextified(g),
    37  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "baseLocalizer", false),
    38  		pipeline:     pipeline,
    39  	}
    40  }
    41  
    42  func (b *baseLocalizer) filterSelfFinalized(ctx context.Context, inbox types.Inbox) (res types.Inbox) {
    43  	username := b.G().Env.GetUsername().String()
    44  	res = inbox
    45  	res.ConvsUnverified = nil
    46  	for _, conv := range inbox.ConvsUnverified {
    47  		if conv.Conv.IsSelfFinalized(username) {
    48  			b.Debug(ctx, "baseLocalizer: skipping own finalized convo: %s", conv.ConvIDStr)
    49  			continue
    50  		}
    51  		res.ConvsUnverified = append(res.ConvsUnverified, conv)
    52  	}
    53  	return res
    54  }
    55  
    56  func (b *baseLocalizer) getConvs(inbox types.Inbox, maxLocalize *int) []types.RemoteConversation {
    57  	convs := inbox.ConvsUnverified
    58  	if maxLocalize == nil || *maxLocalize >= len(convs) {
    59  		return convs
    60  	}
    61  	return convs[:*maxLocalize]
    62  }
    63  
    64  type blockingLocalizer struct {
    65  	globals.Contextified
    66  	utils.DebugLabeler
    67  	*baseLocalizer
    68  
    69  	localizeCb chan types.AsyncInboxResult
    70  }
    71  
    72  func newBlockingLocalizer(g *globals.Context, pipeline *localizerPipeline,
    73  	localizeCb chan types.AsyncInboxResult) *blockingLocalizer {
    74  	return &blockingLocalizer{
    75  		Contextified:  globals.NewContextified(g),
    76  		DebugLabeler:  utils.NewDebugLabeler(g.ExternalG(), "blockingLocalizer", false),
    77  		baseLocalizer: newBaseLocalizer(g, pipeline),
    78  		localizeCb:    localizeCb,
    79  	}
    80  }
    81  
    82  func (b *blockingLocalizer) Localize(ctx context.Context, uid gregor1.UID, inbox types.Inbox,
    83  	maxLocalize *int) (res []chat1.ConversationLocal, err error) {
    84  	defer b.Trace(ctx, &err, "Localize")()
    85  	inbox = b.filterSelfFinalized(ctx, inbox)
    86  	convs := b.getConvs(inbox, maxLocalize)
    87  	if err := b.baseLocalizer.pipeline.queue(ctx, uid, convs, b.localizeCb); err != nil {
    88  		b.Debug(ctx, "Localize: failed to queue: %s", err)
    89  		return res, err
    90  	}
    91  
    92  	res = make([]chat1.ConversationLocal, len(convs))
    93  	indexMap := make(map[chat1.ConvIDStr]int)
    94  	for index, c := range convs {
    95  		indexMap[c.ConvIDStr] = index
    96  	}
    97  	doneCb := make(chan struct{})
    98  	go func() {
    99  		for ar := range b.localizeCb {
   100  			res[indexMap[ar.ConvLocal.GetConvID().ConvIDStr()]] = ar.ConvLocal
   101  		}
   102  		close(doneCb)
   103  	}()
   104  	select {
   105  	case <-doneCb:
   106  	case <-ctx.Done():
   107  		return res, ctx.Err()
   108  	}
   109  	return res, nil
   110  }
   111  
   112  func (b *blockingLocalizer) Name() string {
   113  	return "blocking"
   114  }
   115  
   116  type nonBlockingLocalizer struct {
   117  	globals.Contextified
   118  	utils.DebugLabeler
   119  	*baseLocalizer
   120  
   121  	localizeCb chan types.AsyncInboxResult
   122  }
   123  
   124  func newNonblockingLocalizer(g *globals.Context, pipeline *localizerPipeline,
   125  	localizeCb chan types.AsyncInboxResult) *nonBlockingLocalizer {
   126  	return &nonBlockingLocalizer{
   127  		Contextified:  globals.NewContextified(g),
   128  		DebugLabeler:  utils.NewDebugLabeler(g.ExternalG(), "nonBlockingLocalizer", false),
   129  		baseLocalizer: newBaseLocalizer(g, pipeline),
   130  		localizeCb:    localizeCb,
   131  	}
   132  }
   133  
   134  func (b *nonBlockingLocalizer) filterInboxRes(ctx context.Context, inbox types.Inbox, uid gregor1.UID) (types.Inbox, error) {
   135  	defer b.Trace(ctx, nil, "filterInboxRes")()
   136  	// Loop through and look for empty convs or known errors and skip them
   137  	var res []types.RemoteConversation
   138  	for _, conv := range inbox.ConvsUnverified {
   139  		select {
   140  		case <-ctx.Done():
   141  			return types.Inbox{}, ctx.Err()
   142  		default:
   143  		}
   144  		if utils.IsConvEmpty(conv.Conv) {
   145  			b.Debug(ctx, "filterInboxRes: skipping because empty: convID: %s", conv.Conv.GetConvID())
   146  			continue
   147  		}
   148  		res = append(res, conv)
   149  	}
   150  	return types.Inbox{
   151  		Version:         inbox.Version,
   152  		ConvsUnverified: res,
   153  		Convs:           inbox.Convs,
   154  	}, nil
   155  }
   156  
   157  func (b *nonBlockingLocalizer) Localize(ctx context.Context, uid gregor1.UID, inbox types.Inbox,
   158  	maxLocalize *int) (res []chat1.ConversationLocal, err error) {
   159  	defer b.Trace(ctx, &err, "Localize")()
   160  	// Run some easy filters for empty messages and known errors to optimize UI drawing behavior
   161  	inbox = b.filterSelfFinalized(ctx, inbox)
   162  	filteredInbox, err := b.filterInboxRes(ctx, inbox, uid)
   163  	if err != nil {
   164  		return res, err
   165  	}
   166  	// Send inbox over localize channel
   167  	select {
   168  	case <-ctx.Done():
   169  		return res, ctx.Err()
   170  	case b.localizeCb <- types.AsyncInboxResult{
   171  		InboxRes: &filteredInbox,
   172  	}:
   173  	}
   174  	// Spawn off localization into its own goroutine and use cb to communicate with outside world
   175  	go func(ctx context.Context) {
   176  		b.Debug(ctx, "Localize: starting background localization: convs: %d", len(inbox.ConvsUnverified))
   177  		if err := b.baseLocalizer.pipeline.queue(ctx, uid, b.getConvs(inbox, maxLocalize), b.localizeCb); err != nil {
   178  			b.Debug(ctx, "Localize: failed to queue: %s", err)
   179  			close(b.localizeCb)
   180  		}
   181  	}(globals.BackgroundChatCtx(ctx, b.G()))
   182  	return nil, nil
   183  }
   184  
   185  func (b *nonBlockingLocalizer) Name() string {
   186  	return "nonblocking"
   187  }
   188  
   189  type localizerPipelineJob struct {
   190  	sync.Mutex
   191  
   192  	ctx       context.Context
   193  	cancelFn  context.CancelFunc
   194  	retCh     chan types.AsyncInboxResult
   195  	uid       gregor1.UID
   196  	completed int
   197  	pending   []types.RemoteConversation
   198  
   199  	// testing
   200  	gateCh chan struct{}
   201  }
   202  
   203  func (l *localizerPipelineJob) retry(g *globals.Context) (res *localizerPipelineJob) {
   204  	l.Lock()
   205  	defer l.Unlock()
   206  	res = new(localizerPipelineJob)
   207  	res.ctx, res.cancelFn = context.WithCancel(globals.BackgroundChatCtx(l.ctx, g))
   208  	res.retCh = l.retCh
   209  	res.uid = l.uid
   210  	res.completed = l.completed
   211  	res.pending = make([]types.RemoteConversation, len(l.pending))
   212  	res.gateCh = make(chan struct{})
   213  	copy(res.pending, l.pending)
   214  	return res
   215  }
   216  
   217  func (l *localizerPipelineJob) closeIfDone() bool {
   218  	l.Lock()
   219  	defer l.Unlock()
   220  	if len(l.pending) == 0 {
   221  		close(l.retCh)
   222  		return true
   223  	}
   224  	return false
   225  }
   226  
   227  func (l *localizerPipelineJob) getPending() (res []types.RemoteConversation) {
   228  	l.Lock()
   229  	defer l.Unlock()
   230  	res = make([]types.RemoteConversation, len(l.pending))
   231  	copy(res, l.pending)
   232  	return res
   233  }
   234  
   235  func (l *localizerPipelineJob) numPending() int {
   236  	l.Lock()
   237  	defer l.Unlock()
   238  	return len(l.pending)
   239  }
   240  
   241  func (l *localizerPipelineJob) numCompleted() int {
   242  	l.Lock()
   243  	defer l.Unlock()
   244  	return l.completed
   245  }
   246  
   247  func (l *localizerPipelineJob) complete(convID chat1.ConversationID) {
   248  	l.Lock()
   249  	defer l.Unlock()
   250  	for index, j := range l.pending {
   251  		if j.GetConvID().Eq(convID) {
   252  			l.completed++
   253  			l.pending = append(l.pending[:index], l.pending[index+1:]...)
   254  			return
   255  		}
   256  	}
   257  }
   258  
   259  func newLocalizerPipelineJob(ctx context.Context, g *globals.Context, uid gregor1.UID,
   260  	convs []types.RemoteConversation, retCh chan types.AsyncInboxResult) *localizerPipelineJob {
   261  	return &localizerPipelineJob{
   262  		ctx:     globals.BackgroundChatCtx(ctx, g),
   263  		retCh:   retCh,
   264  		uid:     uid,
   265  		pending: convs,
   266  		gateCh:  make(chan struct{}),
   267  	}
   268  }
   269  
   270  type localizerPipeline struct {
   271  	globals.Contextified
   272  	utils.DebugLabeler
   273  	sync.Mutex
   274  
   275  	offline bool
   276  
   277  	started        bool
   278  	stopCh         chan struct{}
   279  	cancelChs      map[string]chan struct{}
   280  	suspendCount   int
   281  	suspendWaiters []chan struct{}
   282  	jobQueue       chan *localizerPipelineJob
   283  
   284  	// testing
   285  	useGateCh   bool
   286  	jobPulledCh chan *localizerPipelineJob
   287  }
   288  
   289  func newLocalizerPipeline(g *globals.Context) *localizerPipeline {
   290  	return &localizerPipeline{
   291  		Contextified: globals.NewContextified(g),
   292  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "localizerPipeline", false),
   293  		stopCh:       make(chan struct{}),
   294  		cancelChs:    make(map[string]chan struct{}),
   295  	}
   296  }
   297  
   298  func (s *localizerPipeline) Connected() {
   299  	s.Lock()
   300  	defer s.Unlock()
   301  	s.offline = false
   302  }
   303  
   304  func (s *localizerPipeline) Disconnected() {
   305  	s.Lock()
   306  	defer s.Unlock()
   307  	s.offline = true
   308  }
   309  
   310  func (s *localizerPipeline) queue(ctx context.Context, uid gregor1.UID, convs []types.RemoteConversation,
   311  	retCh chan types.AsyncInboxResult) error {
   312  	defer s.Trace(ctx, nil, "queue")()
   313  	s.Lock()
   314  	defer s.Unlock()
   315  	if !s.started {
   316  		return errors.New("localizer not running")
   317  	}
   318  	job := newLocalizerPipelineJob(ctx, s.G(), uid, convs, retCh)
   319  	job.ctx, job.cancelFn = context.WithCancel(globals.BackgroundChatCtx(ctx, s.G()))
   320  	if globals.IsLocalizerCancelableCtx(job.ctx) {
   321  		s.Debug(job.ctx, "queue: adding cancellable job")
   322  	}
   323  	s.jobQueue <- job
   324  	return nil
   325  }
   326  
   327  func (s *localizerPipeline) clearQueue() {
   328  	s.jobQueue = make(chan *localizerPipelineJob, 100)
   329  }
   330  
   331  func (s *localizerPipeline) start(ctx context.Context) {
   332  	defer s.Trace(ctx, nil, "start")()
   333  	s.Lock()
   334  	defer s.Unlock()
   335  	if s.started {
   336  		close(s.stopCh)
   337  		s.stopCh = make(chan struct{})
   338  	}
   339  	s.clearQueue()
   340  	s.started = true
   341  	go s.localizeLoop()
   342  }
   343  
   344  func (s *localizerPipeline) stop(ctx context.Context) chan struct{} {
   345  	defer s.Trace(ctx, nil, "stop")()
   346  	s.Lock()
   347  	defer s.Unlock()
   348  	ch := make(chan struct{})
   349  	if s.started {
   350  		close(s.stopCh)
   351  		s.stopCh = make(chan struct{})
   352  		s.started = false
   353  	}
   354  	close(ch)
   355  	return ch
   356  }
   357  
   358  func (s *localizerPipeline) suspend(ctx context.Context) bool {
   359  	defer s.Trace(ctx, nil, "suspend")()
   360  	s.Lock()
   361  	defer s.Unlock()
   362  	if !s.started {
   363  		return false
   364  	}
   365  	s.suspendCount++
   366  	if len(s.cancelChs) == 0 {
   367  		return false
   368  	}
   369  	for _, ch := range s.cancelChs {
   370  		ch <- struct{}{}
   371  	}
   372  	s.cancelChs = make(map[string]chan struct{})
   373  	return true
   374  }
   375  
   376  func (s *localizerPipeline) registerJobPull(ctx context.Context) (string, chan struct{}) {
   377  	s.Lock()
   378  	defer s.Unlock()
   379  	id := libkb.RandStringB64(3)
   380  	ch := make(chan struct{}, 1)
   381  	if globals.IsLocalizerCancelableCtx(ctx) {
   382  		s.cancelChs[id] = ch
   383  	}
   384  	return id, ch
   385  }
   386  
   387  func (s *localizerPipeline) finishJobPull(id string) {
   388  	s.Lock()
   389  	defer s.Unlock()
   390  	delete(s.cancelChs, id)
   391  }
   392  
   393  func (s *localizerPipeline) resume(ctx context.Context) bool {
   394  	defer s.Trace(ctx, nil, "resume")()
   395  	s.Lock()
   396  	defer s.Unlock()
   397  	if s.suspendCount == 0 {
   398  		s.Debug(ctx, "resume: spurious resume call without suspend")
   399  		return false
   400  	}
   401  	s.suspendCount--
   402  	if s.suspendCount == 0 {
   403  		for _, cb := range s.suspendWaiters {
   404  			close(cb)
   405  		}
   406  		s.suspendWaiters = nil
   407  	}
   408  	return false
   409  }
   410  
   411  func (s *localizerPipeline) registerWaiter() chan struct{} {
   412  	s.Lock()
   413  	defer s.Unlock()
   414  	cb := make(chan struct{})
   415  	if s.suspendCount == 0 {
   416  		close(cb)
   417  		return cb
   418  	}
   419  	s.suspendWaiters = append(s.suspendWaiters, cb)
   420  	return cb
   421  }
   422  
   423  func (s *localizerPipeline) localizeJobPulled(job *localizerPipelineJob, stopCh chan struct{}) {
   424  	id, cancelCh := s.registerJobPull(job.ctx)
   425  	defer s.finishJobPull(id)
   426  	s.Debug(job.ctx, "localizeJobPulled: pulling job: pending: %d completed: %d", job.numPending(),
   427  		job.numCompleted())
   428  	waitCh := make(chan struct{})
   429  	if !globals.IsLocalizerCancelableCtx(job.ctx) {
   430  		close(waitCh)
   431  	} else {
   432  		s.Debug(job.ctx, "localizeJobPulled: waiting for resume")
   433  		go func() {
   434  			<-s.registerWaiter()
   435  			close(waitCh)
   436  		}()
   437  	}
   438  	select {
   439  	case <-waitCh:
   440  		s.Debug(job.ctx, "localizeJobPulled: resume, proceeding")
   441  	case <-stopCh:
   442  		s.Debug(job.ctx, "localizeJobPulled: shutting down")
   443  		return
   444  	}
   445  	s.jobPulled(job.ctx, job)
   446  	doneCh := make(chan struct{})
   447  	go func() {
   448  		defer close(doneCh)
   449  		if err := s.localizeConversations(job); err == context.Canceled {
   450  			// just put this right back if we canceled it
   451  			s.Debug(job.ctx, "localizeJobPulled: re-enqueuing canceled job")
   452  			s.jobQueue <- job.retry(s.G())
   453  		}
   454  		if job.closeIfDone() {
   455  			s.Debug(job.ctx, "localizeJobPulled: all job tasks complete")
   456  		}
   457  	}()
   458  	select {
   459  	case <-doneCh:
   460  		job.cancelFn()
   461  	case <-cancelCh:
   462  		s.Debug(job.ctx, "localizeJobPulled: canceled a live job")
   463  		job.cancelFn()
   464  	case <-stopCh:
   465  		s.Debug(job.ctx, "localizeJobPulled: shutting down")
   466  		job.cancelFn()
   467  		return
   468  	}
   469  	s.Debug(job.ctx, "localizeJobPulled: job pass complete")
   470  }
   471  
   472  func (s *localizerPipeline) localizeLoop() {
   473  	ctx := context.Background()
   474  	s.Debug(ctx, "localizeLoop: starting up")
   475  	s.Lock()
   476  	stopCh := s.stopCh
   477  	s.Unlock()
   478  	for {
   479  		select {
   480  		case job := <-s.jobQueue:
   481  			go s.localizeJobPulled(job, stopCh)
   482  		case <-stopCh:
   483  			s.Debug(ctx, "localizeLoop: shutting down")
   484  			return
   485  		}
   486  	}
   487  }
   488  
   489  func (s *localizerPipeline) gateCheck(ctx context.Context, ch chan struct{}, index int) {
   490  	if s.useGateCh && ch != nil {
   491  		select {
   492  		case <-ch:
   493  			s.Debug(ctx, "localizeConversations: gate check received: %d", index)
   494  		case <-ctx.Done():
   495  		}
   496  	}
   497  }
   498  
   499  func (s *localizerPipeline) jobPulled(ctx context.Context, job *localizerPipelineJob) {
   500  	if s.jobPulledCh != nil {
   501  		s.jobPulledCh <- job
   502  	}
   503  }
   504  
   505  func (s *localizerPipeline) localizeConversations(localizeJob *localizerPipelineJob) (err error) {
   506  	ctx := localizeJob.ctx
   507  	uid := localizeJob.uid
   508  	defer s.Trace(ctx, &err, "localizeConversations")()
   509  
   510  	// Fetch conversation local information in parallel
   511  	eg, ctx := errgroup.WithContext(ctx)
   512  	ctx = libkb.WithLogTag(ctx, "CHTLOCS")
   513  	pending := localizeJob.getPending()
   514  	if len(pending) == 0 {
   515  		return nil
   516  	}
   517  	s.Debug(ctx, "localizeConversations: pending: %d", len(pending))
   518  	convCh := make(chan types.RemoteConversation, len(pending))
   519  	retCh := make(chan chat1.ConversationID, len(pending))
   520  	eg.Go(func() error {
   521  		defer close(convCh)
   522  		for _, conv := range pending {
   523  			select {
   524  			case <-ctx.Done():
   525  				s.Debug(ctx, "localizeConversations: context is done, bailing (producer)")
   526  				return ctx.Err()
   527  			default:
   528  			}
   529  			convCh <- conv
   530  		}
   531  		return nil
   532  	})
   533  	nthreads := s.G().Env.GetChatInboxSourceLocalizeThreads()
   534  	for i := 0; i < nthreads; i++ {
   535  		index := i
   536  		eg.Go(func() error {
   537  			for conv := range convCh {
   538  				s.gateCheck(ctx, localizeJob.gateCh, index)
   539  				s.Debug(ctx, "localizeConversations: localizing: %d convID: %s", index, conv.ConvIDStr)
   540  				convLocal := s.localizeConversation(ctx, uid, conv)
   541  				select {
   542  				case <-ctx.Done():
   543  					s.Debug(ctx, "localizeConversations: context is done, bailing (consumer): %d", index)
   544  					return ctx.Err()
   545  				default:
   546  				}
   547  				retCh <- conv.GetConvID()
   548  				if convLocal.Error != nil {
   549  					s.Debug(ctx, "localizeConversations: error localizing: convID: %s err: %s",
   550  						conv.ConvIDStr, convLocal.Error.Message)
   551  				}
   552  				localizeJob.retCh <- types.AsyncInboxResult{
   553  					ConvLocal: convLocal,
   554  					Conv:      conv,
   555  				}
   556  				s.Debug(ctx, "localizeConversations: localized: %d convID: %s", index, conv.ConvIDStr)
   557  			}
   558  			return nil
   559  		})
   560  	}
   561  	go func() {
   562  		_ = eg.Wait()
   563  		close(retCh)
   564  	}()
   565  	complete := 0
   566  	for convID := range retCh {
   567  		complete++
   568  		s.Debug(ctx, "localizeConversations: complete: %d remaining: %d", complete, len(pending)-complete)
   569  		localizeJob.complete(convID)
   570  	}
   571  	return eg.Wait()
   572  }
   573  
   574  func (s *localizerPipeline) isErrPermanent(err error) bool {
   575  	if uberr, ok := err.(types.UnboxingError); ok {
   576  		return uberr.IsPermanent()
   577  	}
   578  	return false
   579  }
   580  
   581  func getUnverifiedTlfNameForErrors(conversationRemote chat1.Conversation) string {
   582  	var tlfName string
   583  	var latestMsgID chat1.MessageID
   584  	for _, msg := range conversationRemote.MaxMsgSummaries {
   585  		if msg.GetMessageID() > latestMsgID {
   586  			latestMsgID = msg.GetMessageID()
   587  			tlfName = msg.TLFNameExpanded(conversationRemote.Metadata.FinalizeInfo)
   588  		}
   589  	}
   590  	return tlfName
   591  }
   592  
   593  func (s *localizerPipeline) getMinWriterRoleInfoLocal(ctx context.Context, uid gregor1.UID,
   594  	conv chat1.Conversation) (*chat1.ConversationMinWriterRoleInfoLocal, error) {
   595  	if conv.ConvSettings == nil || conv.ReaderInfo == nil {
   596  		return nil, nil
   597  	}
   598  	info := conv.ConvSettings.MinWriterRoleInfo
   599  	if info == nil {
   600  		return nil, nil
   601  	}
   602  
   603  	// NOTE We use the UntrustedTeamRole here since MinWriterRole is based on
   604  	// server trust. A nefarious server could stop our messages by rejecting
   605  	// them or violate the MinWriterRole by allowing them; lying about our role
   606  	// here doesn't help.
   607  	role := conv.ReaderInfo.UntrustedTeamRole
   608  
   609  	// get the changed by username
   610  	name, err := s.G().GetUPAKLoader().LookupUsername(ctx, keybase1.UID(info.Uid.String()))
   611  	if err != nil {
   612  		return nil, err
   613  	}
   614  	return &chat1.ConversationMinWriterRoleInfoLocal{
   615  		Role:        info.Role,
   616  		ChangedBy:   name.String(),
   617  		CannotWrite: !role.IsOrAbove(info.Role),
   618  	}, nil
   619  }
   620  
   621  func (s *localizerPipeline) getConvSettingsLocal(ctx context.Context, uid gregor1.UID,
   622  	conv chat1.Conversation) (*chat1.ConversationSettingsLocal, error) {
   623  	settings := conv.ConvSettings
   624  	if settings == nil {
   625  		return nil, nil
   626  	}
   627  	res := &chat1.ConversationSettingsLocal{}
   628  	minWriterRoleInfo, err := s.getMinWriterRoleInfoLocal(ctx, uid, conv)
   629  	if err != nil {
   630  		return nil, err
   631  	}
   632  	res.MinWriterRoleInfo = minWriterRoleInfo
   633  	return res, nil
   634  }
   635  
   636  // returns an incomplete list in case of error
   637  func (s *localizerPipeline) getResetUsernamesMetadata(ctx context.Context, uidMapper libkb.UIDMapper,
   638  	conv chat1.Conversation) (res []string) {
   639  	if len(conv.Metadata.ResetList) == 0 {
   640  		return res
   641  	}
   642  
   643  	var kuids []keybase1.UID
   644  	for _, uid := range conv.Metadata.ResetList {
   645  		kuids = append(kuids, keybase1.UID(uid.String()))
   646  	}
   647  	rows, err := uidMapper.MapUIDsToUsernamePackages(ctx, s.G(), kuids, 0, 0, false)
   648  	if err != nil {
   649  		s.Debug(ctx, "getResetUsernamesMetadata: failed to run uid mapper: %s", err)
   650  		return res
   651  	}
   652  	for _, row := range rows {
   653  		res = append(res, row.NormalizedUsername.String())
   654  	}
   655  
   656  	return res
   657  }
   658  
   659  func (s *localizerPipeline) getPinnedMsg(ctx context.Context, uid gregor1.UID, conv chat1.Conversation,
   660  	pinMessage chat1.MessageUnboxed) (pinnedMsg chat1.MessageUnboxed, pinnerUsername string, valid bool, err error) {
   661  	defer s.Trace(ctx, &err, "getPinnedMsg: %v", pinMessage.GetMessageID())()
   662  	if !pinMessage.IsValidFull() {
   663  		s.Debug(ctx, "getPinnedMsg: not a valid pin message")
   664  		return pinnedMsg, pinnerUsername, false, nil
   665  	}
   666  	if storage.NewPinIgnore(s.G(), uid).IsIgnored(ctx, conv.GetConvID(), pinMessage.GetMessageID()) {
   667  		s.Debug(ctx, "getPinnedMsg: ignored pinned message")
   668  		return pinnedMsg, pinnerUsername, false, nil
   669  	}
   670  	body := pinMessage.Valid().MessageBody
   671  	pinnedMsgID := body.Pin().MsgID
   672  	messages, err := s.G().ConvSource.GetMessages(ctx, conv.GetConvID(), uid, []chat1.MessageID{pinnedMsgID},
   673  		nil, nil, false)
   674  	if err != nil {
   675  		return pinnedMsg, pinnerUsername, false, nil
   676  	}
   677  	maxDeletedUpTo := conv.GetMaxDeletedUpTo()
   678  	xformRes, err := s.G().ConvSource.TransformSupersedes(ctx, conv.GetConvID(), uid, messages,
   679  		&chat1.GetThreadQuery{
   680  			EnableDeletePlaceholders: true,
   681  		}, nil, nil, &maxDeletedUpTo)
   682  	if err != nil {
   683  		return pinnedMsg, pinnerUsername, false, nil
   684  	}
   685  	if len(xformRes) == 0 {
   686  		s.Debug(ctx, "getPinnedMsg: no pin message after xform supersedes")
   687  		return pinnedMsg, pinnerUsername, false, nil
   688  	}
   689  	pinnedMsg = xformRes[0]
   690  	if !pinnedMsg.IsValidFull() {
   691  		s.Debug(ctx, "getPinnedMsg: not a valid pinned message")
   692  		return pinnedMsg, pinnerUsername, false, nil
   693  	}
   694  	return pinnedMsg, pinMessage.Valid().SenderUsername, true, nil
   695  }
   696  
   697  func (s *localizerPipeline) localizeConversation(ctx context.Context, uid gregor1.UID,
   698  	rc types.RemoteConversation) (conversationLocal chat1.ConversationLocal) {
   699  	ctx = globals.CtxModifyUnboxMode(ctx, types.UnboxModeQuick)
   700  	ctx = libkb.WithLogTag(ctx, "CHTLOC")
   701  	conversationRemote := rc.Conv
   702  	unverifiedTLFName := getUnverifiedTlfNameForErrors(conversationRemote)
   703  	defer s.Trace(ctx, nil,
   704  		"localizeConversation: TLF: %s convID: %s offline: %v vis: %v", unverifiedTLFName,
   705  		conversationRemote.GetConvID(), s.offline, conversationRemote.Metadata.Visibility)()
   706  
   707  	var err error
   708  	umapper := s.G().UIDMapper
   709  	conversationLocal.Info = chat1.ConversationInfoLocal{
   710  		Id:            conversationRemote.Metadata.ConversationID,
   711  		IsDefaultConv: conversationRemote.Metadata.IsDefaultConv,
   712  		Visibility:    conversationRemote.Metadata.Visibility,
   713  		Triple:        conversationRemote.Metadata.IdTriple,
   714  		Status:        conversationRemote.Metadata.Status,
   715  		MembersType:   conversationRemote.Metadata.MembersType,
   716  		MemberStatus:  conversationRemote.ReaderInfo.Status,
   717  		TeamType:      conversationRemote.Metadata.TeamType,
   718  		Version:       conversationRemote.Metadata.Version,
   719  		LocalVersion:  conversationRemote.Metadata.LocalVersion,
   720  		FinalizeInfo:  conversationRemote.Metadata.FinalizeInfo,
   721  		Draft:         rc.LocalDraft,
   722  	}
   723  	conversationLocal.BotAliases = make(map[string]string)
   724  	conversationLocal.BotCommands = chat1.NewConversationCommandGroupsWithNone()
   725  	conversationLocal.Supersedes = append(
   726  		conversationLocal.Supersedes, conversationRemote.Metadata.Supersedes...)
   727  	conversationLocal.SupersededBy = append(
   728  		conversationLocal.SupersededBy, conversationRemote.Metadata.SupersededBy...)
   729  	if conversationRemote.ReaderInfo == nil {
   730  		errMsg := "empty ReaderInfo from server?"
   731  		conversationLocal.Error = chat1.NewConversationErrorLocal(
   732  			errMsg, conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
   733  		return conversationLocal
   734  	}
   735  	conversationLocal.ReaderInfo = *conversationRemote.ReaderInfo
   736  	conversationLocal.Notifications = conversationRemote.Notifications
   737  	if conversationRemote.CreatorInfo != nil {
   738  		packages, err := umapper.MapUIDsToUsernamePackages(ctx, s.G(),
   739  			[]keybase1.UID{keybase1.UID(conversationRemote.CreatorInfo.Uid.String())}, 0, 0, false)
   740  		if err != nil || len(packages) == 0 {
   741  			s.Debug(ctx, "localizeConversation: failed to load creator username: %s", err)
   742  		} else {
   743  			conversationLocal.CreatorInfo = &chat1.ConversationCreatorInfoLocal{
   744  				Username: packages[0].NormalizedUsername.String(),
   745  				Ctime:    conversationRemote.CreatorInfo.Ctime,
   746  			}
   747  		}
   748  	}
   749  	conversationLocal.Expunge = conversationRemote.Expunge
   750  	conversationLocal.ConvRetention = conversationRemote.ConvRetention
   751  	conversationLocal.TeamRetention = conversationRemote.TeamRetention
   752  	convSettings, err := s.getConvSettingsLocal(ctx, uid, conversationRemote)
   753  	if err != nil {
   754  		conversationLocal.Error = chat1.NewConversationErrorLocal(
   755  			err.Error(), conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
   756  		return conversationLocal
   757  	}
   758  	conversationLocal.ConvSettings = convSettings
   759  
   760  	if len(conversationRemote.MaxMsgSummaries) == 0 {
   761  		errMsg := "conversation has an empty MaxMsgSummaries field"
   762  		conversationLocal.Error = chat1.NewConversationErrorLocal(
   763  			errMsg, conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
   764  		return conversationLocal
   765  	}
   766  	conversationLocal.MaxMessages = conversationRemote.MaxMsgSummaries
   767  
   768  	conversationLocal.IsEmpty = utils.IsConvEmpty(conversationRemote)
   769  	errTyp := chat1.ConversationErrorType_PERMANENT
   770  	var maxMsgs []chat1.MessageUnboxed
   771  	if len(conversationRemote.MaxMsgs) == 0 {
   772  		// Fetch max messages unboxed, using either a custom function or through
   773  		// the conversation source configured in the global context
   774  		var summaries []chat1.MessageSummary
   775  		snippetSummary, err := utils.PickLatestMessageSummary(conversationRemote, chat1.SnippetChatMessageTypes())
   776  		if err == nil {
   777  			summaries = append(summaries, snippetSummary)
   778  		}
   779  		topicNameSummary, err := conversationRemote.GetMaxMessage(chat1.MessageType_METADATA)
   780  		if err == nil {
   781  			summaries = append(summaries, topicNameSummary)
   782  		}
   783  		headlineSummary, err := conversationRemote.GetMaxMessage(chat1.MessageType_HEADLINE)
   784  		if err == nil {
   785  			summaries = append(summaries, headlineSummary)
   786  		}
   787  		pinSummary, err := conversationRemote.GetMaxMessage(chat1.MessageType_PIN)
   788  		if err == nil {
   789  			summaries = append(summaries, pinSummary)
   790  		}
   791  		if len(summaries) == 0 ||
   792  			conversationRemote.GetMembersType() == chat1.ConversationMembersType_IMPTEAMUPGRADE ||
   793  			conversationRemote.GetMembersType() == chat1.ConversationMembersType_KBFS {
   794  			tlfSummary, err := conversationRemote.GetMaxMessage(chat1.MessageType_TLFNAME)
   795  			if err == nil {
   796  				summaries = append(summaries, tlfSummary)
   797  			}
   798  		}
   799  		reason := chat1.GetThreadReason_LOCALIZE
   800  		msgs, err := s.G().ConvSource.GetMessages(ctx, conversationRemote.GetConvID(),
   801  			uid, utils.PluckMessageIDs(summaries), &reason, nil, false)
   802  		if !s.isErrPermanent(err) {
   803  			errTyp = chat1.ConversationErrorType_TRANSIENT
   804  		}
   805  		if err != nil {
   806  			convErr := s.checkRekeyError(ctx, err, conversationRemote, unverifiedTLFName)
   807  			if convErr != nil {
   808  				conversationLocal.Error = convErr
   809  				return conversationLocal
   810  			}
   811  			conversationLocal.Error = chat1.NewConversationErrorLocal(
   812  				err.Error(), conversationRemote, unverifiedTLFName, errTyp, nil)
   813  			return conversationLocal
   814  		}
   815  		maxMsgs = msgs
   816  	} else {
   817  		// Use the attached MaxMsgs
   818  		msgs, err := s.G().ConvSource.GetMessagesWithRemotes(ctx,
   819  			conversationRemote, uid, conversationRemote.MaxMsgs)
   820  		if err != nil {
   821  			convErr := s.checkRekeyError(ctx, err, conversationRemote, unverifiedTLFName)
   822  			if convErr != nil {
   823  				conversationLocal.Error = convErr
   824  				return conversationLocal
   825  			}
   826  			if !s.isErrPermanent(err) {
   827  				errTyp = chat1.ConversationErrorType_TRANSIENT
   828  			}
   829  			conversationLocal.Error = chat1.NewConversationErrorLocal(
   830  				err.Error(), conversationRemote, unverifiedTLFName, errTyp, nil)
   831  			return conversationLocal
   832  		}
   833  		maxMsgs = msgs
   834  	}
   835  
   836  	var maxValidID chat1.MessageID
   837  	s.Debug(ctx, "localizing %d max msgs", len(maxMsgs))
   838  	for _, mm := range maxMsgs {
   839  		if mm.IsValid() &&
   840  			utils.IsSnippetChatMessageType(mm.GetMessageType()) &&
   841  			(conversationLocal.Info.SnippetMsg == nil ||
   842  				conversationLocal.Info.SnippetMsg.GetMessageID() < mm.GetMessageID()) {
   843  			conversationLocal.Info.SnippetMsg = new(chat1.MessageUnboxed)
   844  			*conversationLocal.Info.SnippetMsg = mm
   845  		}
   846  		if mm.IsValid() {
   847  			body := mm.Valid().MessageBody
   848  			typ, err := body.MessageType()
   849  			if err != nil {
   850  				s.Debug(ctx, "failed to get message type: convID: %s id: %d",
   851  					conversationRemote.GetConvID(), mm.GetMessageID())
   852  				continue
   853  			}
   854  			switch typ {
   855  			case chat1.MessageType_METADATA:
   856  				conversationLocal.Info.TopicName = body.Metadata().ConversationTitle
   857  			case chat1.MessageType_HEADLINE:
   858  				conversationLocal.Info.Headline = body.Headline().Headline
   859  				emojis := body.Headline().Emojis
   860  				headlineEmojis := make([]chat1.HarvestedEmoji, 0, len(emojis))
   861  				for _, emoji := range emojis {
   862  					headlineEmojis = append(headlineEmojis, emoji)
   863  				}
   864  				conversationLocal.Info.HeadlineEmojis = headlineEmojis
   865  			case chat1.MessageType_PIN:
   866  				pinnedMsg, pinnerUsername, valid, err := s.getPinnedMsg(ctx, uid, conversationRemote, mm)
   867  				if err != nil {
   868  					conversationLocal.Error = chat1.NewConversationErrorLocal(
   869  						fmt.Sprintf("unable to get pinned message: %s", err),
   870  						conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
   871  					return conversationLocal
   872  				}
   873  				if valid {
   874  					conversationLocal.Info.PinnedMsg = &chat1.ConversationPinnedMessage{
   875  						Message:        pinnedMsg,
   876  						PinnerUsername: pinnerUsername,
   877  					}
   878  				}
   879  			}
   880  			if mm.GetMessageID() >= maxValidID {
   881  				conversationLocal.Info.Triple = mm.Valid().ClientHeader.Conv
   882  				conversationLocal.Info.TlfName = mm.Valid().ClientHeader.TlfName
   883  				maxValidID = mm.GetMessageID()
   884  			}
   885  		} else {
   886  			s.Debug(ctx, "skipping invalid max msg: state: %v", mm.DebugString())
   887  		}
   888  	}
   889  	// see if we should override the snippet message with the latest outbox record
   890  	obrs, err := storage.NewOutbox(s.G(), uid).PullForConversation(ctx, conversationRemote.GetConvID())
   891  	if err != nil {
   892  		s.Debug(ctx, "unable to get outbox records: %v", err)
   893  	}
   894  	for index := len(obrs) - 1; index >= 0; index-- {
   895  		msg := chat1.NewMessageUnboxedWithOutbox(obrs[index])
   896  		if msg.IsVisible() {
   897  			conversationLocal.Info.SnippetMsg = &msg
   898  			break
   899  		}
   900  	}
   901  
   902  	// Resolve edits/deletes on snippet message
   903  	if conversationLocal.Info.SnippetMsg != nil {
   904  		maxDeletedUpTo := conversationRemote.GetMaxDeletedUpTo()
   905  		superXform := newBasicSupersedesTransform(s.G(), basicSupersedesTransformOpts{})
   906  		if newMsg, err := superXform.Run(ctx, conversationRemote.GetConvID(), uid,
   907  			[]chat1.MessageUnboxed{*conversationLocal.Info.SnippetMsg}, &maxDeletedUpTo); err != nil {
   908  			s.Debug(ctx, "failed to transform message: id: %d err: %s",
   909  				conversationLocal.Info.SnippetMsg.GetMessageID(), err)
   910  		} else {
   911  			if len(newMsg) > 0 {
   912  				conversationLocal.Info.SnippetMsg = &newMsg[0]
   913  			}
   914  		}
   915  	}
   916  
   917  	// Verify ConversationID is derivable from ConversationIDTriple
   918  	if !conversationLocal.Info.Triple.Derivable(conversationLocal.Info.Id) {
   919  		errMsg := fmt.Sprintf("unexpected response from server: conversation ID is not derivable from conversation triple. triple: %#+v; Id: %x",
   920  			conversationLocal.Info.Triple, conversationLocal.Info.Id)
   921  		conversationLocal.Error = chat1.NewConversationErrorLocal(
   922  			errMsg, conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
   923  		return conversationLocal
   924  	}
   925  
   926  	// verify Conv matches ConversationIDTriple in MessageClientHeader
   927  	if !conversationRemote.Metadata.IdTriple.Eq(conversationLocal.Info.Triple) {
   928  		errMsg := "server header conversation triple does not match client header triple"
   929  		conversationLocal.Error = chat1.NewConversationErrorLocal(
   930  			errMsg, conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
   931  		return conversationLocal
   932  	}
   933  
   934  	membersType := conversationRemote.GetMembersType()
   935  	infoSource := CreateNameInfoSource(ctx, s.G(), conversationLocal.GetMembersType())
   936  	var info types.NameInfo
   937  	var ierr error
   938  	switch membersType {
   939  	case chat1.ConversationMembersType_TEAM, chat1.ConversationMembersType_IMPTEAMNATIVE,
   940  		chat1.ConversationMembersType_IMPTEAMUPGRADE:
   941  		tlfName := conversationLocal.Info.TlfName
   942  		if tlfName == "" {
   943  			tlfName = unverifiedTLFName
   944  		}
   945  		info, ierr = infoSource.LookupName(ctx,
   946  			conversationLocal.Info.Triple.Tlfid,
   947  			conversationLocal.Info.Visibility == keybase1.TLFVisibility_PUBLIC,
   948  			tlfName,
   949  		)
   950  	default:
   951  		if len(conversationLocal.Info.TlfName) == 0 {
   952  			conversationLocal.Error = chat1.NewConversationErrorLocal(
   953  				"unable to get conversation name from message history", conversationRemote,
   954  				unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
   955  			return conversationLocal
   956  		}
   957  		info, ierr = infoSource.LookupID(ctx,
   958  			conversationLocal.Info.TlfName,
   959  			conversationLocal.Info.Visibility == keybase1.TLFVisibility_PUBLIC)
   960  	}
   961  	if ierr != nil {
   962  		errMsg := ierr.Error()
   963  		conversationLocal.Error = chat1.NewConversationErrorLocal(
   964  			errMsg, conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT,
   965  			nil)
   966  		return conversationLocal
   967  	}
   968  	conversationLocal.Info.TlfName = info.CanonicalName
   969  
   970  	// Get conversation commands
   971  	conversationLocal.Commands, err = s.G().CommandsSource.ListCommands(ctx, uid, conversationLocal)
   972  	if err != nil {
   973  		s.Debug(ctx, "localizeConversation: failed to list commands: %s", err)
   974  	}
   975  	botCommands, alias, err := s.G().BotCommandManager.ListCommands(ctx, conversationLocal.GetConvID())
   976  	if err != nil {
   977  		s.Debug(ctx, "localizeConversation: failed to list bot commands: %s", err)
   978  		conversationLocal.BotAliases = make(map[string]string)
   979  		conversationLocal.BotCommands = chat1.NewConversationCommandGroupsWithNone()
   980  	} else {
   981  		conversationLocal.BotAliases = alias
   982  		if len(botCommands) > 0 {
   983  			conversationLocal.BotCommands = bots.MakeConversationCommandGroups(botCommands)
   984  		} else {
   985  			conversationLocal.BotCommands = chat1.NewConversationCommandGroupsWithNone()
   986  		}
   987  	}
   988  
   989  	// Form the writers name list, either from the active list + TLF name, or from the
   990  	// channel information for a team chat
   991  	switch membersType {
   992  	case chat1.ConversationMembersType_TEAM:
   993  		// do nothing
   994  	case chat1.ConversationMembersType_IMPTEAMNATIVE, chat1.ConversationMembersType_IMPTEAMUPGRADE:
   995  		conversationLocal.Info.ResetNames = utils.DedupStringLists(
   996  			s.getResetUsernamesMetadata(ctx, umapper, conversationRemote),
   997  			nil,
   998  		)
   999  		var kuids []keybase1.UID
  1000  		for _, uid := range info.VerifiedMembers {
  1001  			kuids = append(kuids, keybase1.UID(uid.String()))
  1002  		}
  1003  		rows, err := umapper.MapUIDsToUsernamePackages(ctx, s.G(), kuids, time.Hour*24, 10*time.Second, true)
  1004  		if err != nil {
  1005  			s.Debug(ctx, "localizeConversation: impteam UIDMapper returned an error: %s", err)
  1006  			errMsg := fmt.Sprintf("error getting usernames of participants: %s", err)
  1007  			conversationLocal.Error = chat1.NewConversationErrorLocal(
  1008  				errMsg, conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
  1009  			return conversationLocal
  1010  		}
  1011  		var verifiedUsernames []string
  1012  		for _, row := range rows {
  1013  			verifiedUsernames = append(verifiedUsernames, row.NormalizedUsername.String())
  1014  		}
  1015  		conversationLocal.Info.Participants, err = utils.ReorderParticipants(
  1016  			s.G().MetaContext(ctx),
  1017  			s.G(),
  1018  			umapper,
  1019  			conversationLocal.Info.TlfName,
  1020  			verifiedUsernames,
  1021  			conversationRemote.Metadata.ActiveList)
  1022  		if err != nil {
  1023  			errMsg := fmt.Sprintf("error reordering participants: %s", err)
  1024  			conversationLocal.Error = chat1.NewConversationErrorLocal(
  1025  				errMsg, conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
  1026  			return conversationLocal
  1027  		}
  1028  		utils.AttachContactNames(s.G().MetaContext(ctx), conversationLocal.Info.Participants)
  1029  	case chat1.ConversationMembersType_KBFS:
  1030  		conversationLocal.Info.Participants, err = utils.ReorderParticipantsKBFS(
  1031  			s.G().MetaContext(ctx),
  1032  			s.G(),
  1033  			umapper,
  1034  			conversationLocal.Info.TlfName,
  1035  			conversationRemote.Metadata.ActiveList)
  1036  		if err != nil {
  1037  			errMsg := fmt.Sprintf("error reordering participants: %s", err)
  1038  			conversationLocal.Error = chat1.NewConversationErrorLocal(
  1039  				errMsg, conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
  1040  			return conversationLocal
  1041  		}
  1042  		utils.AttachContactNames(s.G().MetaContext(ctx), conversationLocal.Info.Participants)
  1043  	default:
  1044  		conversationLocal.Error = chat1.NewConversationErrorLocal(
  1045  			"unknown members type", conversationRemote, unverifiedTLFName,
  1046  			chat1.ConversationErrorType_PERMANENT, nil)
  1047  		return conversationLocal
  1048  	}
  1049  	return conversationLocal
  1050  }
  1051  
  1052  // Checks fromErr to see if it is a rekey error.
  1053  // Returns a ConversationErrorLocal if it is a rekey error.
  1054  // Returns nil otherwise.
  1055  func (s *localizerPipeline) checkRekeyError(ctx context.Context, fromErr error, conversationRemote chat1.Conversation, unverifiedTLFName string) *chat1.ConversationErrorLocal {
  1056  	if fromErr == nil {
  1057  		return nil
  1058  	}
  1059  	convErr, err2 := s.checkRekeyErrorInner(ctx, fromErr, conversationRemote, unverifiedTLFName)
  1060  	if err2 != nil {
  1061  		errMsg := fmt.Sprintf("failed to get rekey info: convID: %s: %s",
  1062  			conversationRemote.Metadata.ConversationID, err2.Error())
  1063  		return chat1.NewConversationErrorLocal(
  1064  			errMsg, conversationRemote, unverifiedTLFName, chat1.ConversationErrorType_TRANSIENT, nil)
  1065  	}
  1066  	if convErr != nil {
  1067  		return convErr
  1068  	}
  1069  	return nil
  1070  }
  1071  
  1072  // Checks fromErr to see if it is a rekey error.
  1073  // Returns (ConversationErrorRekey, nil) if it is
  1074  // Returns (nil, nil) if it is a different kind of error
  1075  // Returns (nil, err) if there is an error building the ConversationErrorRekey
  1076  func (s *localizerPipeline) checkRekeyErrorInner(ctx context.Context, fromErr error, conversationRemote chat1.Conversation, unverifiedTLFName string) (*chat1.ConversationErrorLocal, error) {
  1077  	var rekeyInfo *chat1.ConversationErrorRekey
  1078  	var ok bool
  1079  
  1080  	// check for rekey error type
  1081  	var convErrTyp chat1.ConversationErrorType
  1082  	if convErrTyp, ok = IsRekeyError(fromErr); !ok {
  1083  		return nil, nil
  1084  	}
  1085  	rekeyInfo = &chat1.ConversationErrorRekey{
  1086  		TlfName: unverifiedTLFName,
  1087  	}
  1088  
  1089  	if len(conversationRemote.MaxMsgSummaries) == 0 {
  1090  		return nil, errors.New("can't determine isPrivate with no maxMsgs")
  1091  	}
  1092  	rekeyInfo.TlfPublic = conversationRemote.MaxMsgSummaries[0].TlfPublic
  1093  
  1094  	// Fill readers and writers
  1095  	parts, err := utils.ReorderParticipantsKBFS(
  1096  		s.G().MetaContext(ctx),
  1097  		s.G(),
  1098  		s.G().UIDMapper,
  1099  		rekeyInfo.TlfName,
  1100  		conversationRemote.Metadata.ActiveList)
  1101  	if err != nil {
  1102  		return nil, err
  1103  	}
  1104  	var writerNames []string
  1105  	for _, p := range parts {
  1106  		writerNames = append(writerNames, p.Username)
  1107  	}
  1108  	rekeyInfo.WriterNames = writerNames
  1109  
  1110  	// Fill rekeyers list
  1111  	myUsername := string(s.G().Env.GetUsername())
  1112  	rekeyExcludeSelf := (convErrTyp != chat1.ConversationErrorType_SELFREKEYNEEDED)
  1113  	for _, w := range writerNames {
  1114  		if rekeyExcludeSelf && w == myUsername {
  1115  			// Skip self if self can't rekey.
  1116  			continue
  1117  		}
  1118  		if strings.Contains(w, "@") {
  1119  			// Skip assertions. They can't rekey.
  1120  			continue
  1121  		}
  1122  		rekeyInfo.Rekeyers = append(rekeyInfo.Rekeyers, w)
  1123  	}
  1124  
  1125  	convErrorLocal := chat1.NewConversationErrorLocal(
  1126  		fromErr.Error(), conversationRemote, unverifiedTLFName, convErrTyp, rekeyInfo)
  1127  	return convErrorLocal, nil
  1128  }