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