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