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