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 }