github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/home/home.go (about) 1 // Copyright 2019 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package home 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "math/rand" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/keybase/client/go/contacts" 15 16 "github.com/keybase/client/go/gregor" 17 "github.com/keybase/client/go/libkb" 18 "github.com/keybase/client/go/protocol/gregor1" 19 keybase1 "github.com/keybase/client/go/protocol/keybase1" 20 "golang.org/x/net/context" 21 ) 22 23 type homeCache struct { 24 obj keybase1.HomeScreen 25 cachedAt time.Time 26 } 27 28 type peopleCache struct { 29 all []keybase1.HomeUserSummary 30 lastShown []keybase1.HomeUserSummary 31 cachedAt time.Time 32 } 33 34 type Home struct { 35 libkb.Contextified 36 37 sync.Mutex 38 homeCache *homeCache 39 peopleCache *peopleCache 40 } 41 42 type rawGetHome struct { 43 libkb.AppStatusEmbed 44 Home keybase1.HomeScreen `json:"home"` 45 } 46 47 func NewHome(g *libkb.GlobalContext) *Home { 48 home := &Home{Contextified: libkb.NewContextified(g)} 49 g.AddLogoutHook(home, "home") 50 g.AddDbNukeHook(home, "home") 51 return home 52 } 53 54 func homeRetry(a libkb.APIArg) libkb.APIArg { 55 a.RetryCount = 3 56 a.InitialTimeout = 4 * time.Second 57 a.RetryMultiplier = 1.1 58 return a 59 } 60 61 func decodeContactNotifications(mctx libkb.MetaContext, home keybase1. 62 HomeScreen) (decoded keybase1.HomeScreen, err error) { 63 items := home.Items 64 for i, item := range items { 65 t, err := item.Data.T() 66 if err != nil { 67 mctx.Warning("Could not determine home screen item type %v: %v", 68 item, err) 69 continue 70 } 71 if t == keybase1.HomeScreenItemType_PEOPLE { 72 peopleItem := item.Data.People() 73 innerT, err := peopleItem.T() 74 if err != nil { 75 mctx.Warning( 76 "Could not determine home screen inner item type %v: %v", 77 item, err) 78 continue 79 } 80 if innerT == keybase1.HomeScreenPeopleNotificationType_CONTACT { 81 contact := peopleItem.Contact() 82 decryptedContact, 83 err := contacts.DecryptContactBlob(mctx, 84 contact.ResolvedContactBlob) 85 if err != nil { 86 return home, err 87 } 88 89 contact.Username = decryptedContact.ResolvedUser.Username 90 contact.Description = decryptedContact.Description 91 item.Data = keybase1.NewHomeScreenItemDataWithPeople( 92 keybase1.NewHomeScreenPeopleNotificationWithContact(contact)) 93 items[i] = item 94 } else if innerT == keybase1.HomeScreenPeopleNotificationType_CONTACT_MULTI { 95 contactMulti := peopleItem.ContactMulti() 96 contactList := contactMulti.Contacts 97 for i, contact := range contactList { 98 decryptedContact, 99 err := contacts.DecryptContactBlob(mctx, 100 contact.ResolvedContactBlob) 101 if err != nil { 102 return home, err 103 } 104 105 contactList[i].Username = decryptedContact.ResolvedUser.Username 106 contactList[i].Description = decryptedContact.Description 107 } 108 item.Data = keybase1.NewHomeScreenItemDataWithPeople( 109 keybase1.NewHomeScreenPeopleNotificationWithContactMulti( 110 contactMulti)) 111 items[i] = item 112 } 113 } 114 } 115 116 home.Items = items 117 return home, nil 118 } 119 120 func (h *Home) getToCache(ctx context.Context, markedViewed bool, numPeopleWanted int, skipPeople bool) (err error) { 121 mctx := libkb.NewMetaContext(ctx, h.G()) 122 defer mctx.Trace("Home#getToCache", &err)() 123 124 numPeopleToRequest := 100 125 if numPeopleWanted > numPeopleToRequest { 126 numPeopleToRequest = numPeopleWanted 127 } 128 if skipPeople { 129 numPeopleToRequest = 0 130 } 131 arg := libkb.NewAPIArg("home") 132 arg.SessionType = libkb.APISessionTypeREQUIRED 133 arg.Args = libkb.HTTPArgs{ 134 "record_visit": libkb.B{Val: markedViewed}, 135 "num_people": libkb.I{Val: numPeopleToRequest}, 136 } 137 var raw rawGetHome 138 if err = mctx.G().API.GetDecode(mctx, homeRetry(arg), &raw); err != nil { 139 return err 140 } 141 home, err := decodeContactNotifications(mctx, raw.Home) 142 if err != nil { 143 return err 144 } 145 146 newPeopleCache := &peopleCache{ 147 all: home.FollowSuggestions, 148 } 149 150 if h.peopleCache != nil { 151 newPeopleCache.lastShown = h.peopleCache.lastShown 152 newPeopleCache.cachedAt = h.peopleCache.cachedAt 153 } 154 h.peopleCache = newPeopleCache 155 156 mctx.Debug("| %d follow suggestions returned", len(home.FollowSuggestions)) 157 home.FollowSuggestions = nil 158 159 h.homeCache = &homeCache{ 160 obj: home, 161 cachedAt: h.G().GetClock().Now(), 162 } 163 164 return nil 165 } 166 167 func (h *Home) Get(ctx context.Context, markViewed bool, numPeopleWanted int) (ret keybase1.HomeScreen, err error) { 168 ctx = libkb.WithLogTag(ctx, "HOME") 169 defer h.G().CTrace(ctx, "Home#Get", &err)() 170 171 // 10 people by default 172 if numPeopleWanted < 0 { 173 numPeopleWanted = 10 174 } 175 176 h.Lock() 177 defer h.Unlock() 178 179 useCache, people := h.peopleCache.isValid(ctx, h.G(), numPeopleWanted) 180 if useCache { 181 useCache = h.homeCache.isValid(ctx, h.G()) 182 } 183 184 if useCache && markViewed { 185 err := h.bustHomeCacheIfBadgedFollowers(ctx) 186 if err != nil { 187 return ret, err 188 } 189 useCache = h.homeCache != nil 190 // If we blew up our cache, get out of here and refetch, proceed with 191 // marking the view. 192 if useCache { 193 h.G().Log.CDebugf(ctx, "| cache is good; going to server to mark view") 194 if err := h.markViewedAPICall(ctx); err != nil { 195 h.G().Log.CInfof(ctx, "Error marking home as viewed: %s", err.Error()) 196 } 197 } 198 } 199 200 if !useCache { 201 h.G().Log.CDebugf(ctx, "| cache is no good; going fetching from server") 202 // If we've already found the people we need to show in the cache, 203 // there's no reason to reload them. 204 skipLoadPeople := len(people) > 0 205 if err = h.getToCache(ctx, markViewed, numPeopleWanted, skipLoadPeople); err != nil { 206 return ret, err 207 } 208 } 209 210 // Prime the return object with whatever was cached for home 211 tmp := h.homeCache.obj 212 213 if people != nil { 214 tmp.FollowSuggestions = people 215 } else { 216 err := h.peopleCache.loadInto(ctx, h.G(), &tmp, numPeopleWanted) 217 if err != nil { 218 return ret, err 219 } 220 } 221 222 // Return a deep copy of the tmp object, so that the caller can't 223 // change it or race against other Go routines. 224 ret = tmp.DeepCopy() 225 226 return ret, nil 227 } 228 229 func (p *peopleCache) loadInto(ctx context.Context, g *libkb.GlobalContext, out *keybase1.HomeScreen, numPeopleWanted int) error { 230 if numPeopleWanted > len(p.all) { 231 numPeopleWanted = len(p.all) 232 g.Log.CDebugf(ctx, "| didn't get enough people loaded, so short-changing at %d", numPeopleWanted) 233 } 234 out.FollowSuggestions = p.all[0:numPeopleWanted] 235 p.all = p.all[numPeopleWanted:] 236 p.lastShown = out.FollowSuggestions 237 p.cachedAt = g.GetClock().Now() 238 return nil 239 } 240 241 func (h *homeCache) isValid(ctx context.Context, g *libkb.GlobalContext) bool { 242 if h == nil { 243 g.Log.CDebugf(ctx, "| homeCache == nil, therefore isn't valid") 244 return false 245 } 246 diff := g.GetClock().Now().Sub(h.cachedAt) 247 if diff >= libkb.HomeCacheTimeout { 248 g.Log.CDebugf(ctx, "| homeCache was stale (cached %s ago)", diff) 249 return false 250 } 251 g.Log.CDebugf(ctx, "| homeCache was valid (cached %s ago)", diff) 252 return true 253 } 254 255 func (p *peopleCache) isValid(ctx context.Context, g *libkb.GlobalContext, numPeopleWanted int) (bool, []keybase1.HomeUserSummary) { 256 if p == nil { 257 g.Log.CDebugf(ctx, "| peopleCache = nil, therefore isn't valid") 258 return false, nil 259 } 260 diff := g.GetClock().Now().Sub(p.cachedAt) 261 if diff < libkb.HomePeopleCacheTimeout && numPeopleWanted <= len(p.lastShown) { 262 g.Log.CDebugf(ctx, "| peopleCache is valid, just returning last viewed") 263 return true, p.lastShown 264 } 265 if numPeopleWanted <= len(p.all) { 266 g.Log.CDebugf(ctx, "| people cache is valid, can pop from all") 267 return true, nil 268 } 269 return false, nil 270 } 271 272 func (h *Home) skipTodoType(ctx context.Context, typ keybase1.HomeScreenTodoType) (err error) { 273 mctx := libkb.NewMetaContext(ctx, h.G()) 274 defer mctx.Trace("Home#skipTodoType", &err)() 275 276 _, err = mctx.G().API.Post(mctx, homeRetry(libkb.APIArg{ 277 Endpoint: "home/todo/skip", 278 SessionType: libkb.APISessionTypeREQUIRED, 279 Args: libkb.HTTPArgs{ 280 "type": libkb.I{Val: int(typ)}, 281 }, 282 })) 283 284 return err 285 } 286 287 func (h *Home) DismissAnnouncement(ctx context.Context, id keybase1.HomeScreenAnnouncementID) (err error) { 288 mctx := libkb.NewMetaContext(ctx, h.G()) 289 defer mctx.Trace("Home#DismissAnnouncement", &err)() 290 291 _, err = mctx.G().API.Post(mctx, homeRetry(libkb.APIArg{ 292 Endpoint: "home/todo/skip", 293 SessionType: libkb.APISessionTypeREQUIRED, 294 Args: libkb.HTTPArgs{ 295 "announcement": libkb.I{Val: int(id)}, 296 }, 297 })) 298 299 return err 300 } 301 302 func (h *Home) bustCache(ctx context.Context, bustPeople bool) { 303 h.G().Log.CDebugf(ctx, "Home#bustCache") 304 h.Lock() 305 defer h.Unlock() 306 h.homeCache = nil 307 if bustPeople { 308 h.peopleCache = nil 309 } 310 } 311 312 func (h *Home) bustHomeCacheIfBadgedFollowers(ctx context.Context) (err error) { 313 defer h.G().CTrace(ctx, "+ Home#bustHomeCacheIfBadgedFollowers", &err)() 314 315 if h.homeCache == nil { 316 h.G().Log.CDebugf(ctx, "| nil home cache, nothing to bust") 317 return nil 318 } 319 320 bust := false 321 for i, item := range h.homeCache.obj.Items { 322 if !item.Badged { 323 continue 324 } 325 if typ, err := item.Data.T(); err != nil { 326 bust = true 327 h.G().Log.CDebugf(ctx, "| in bustHomeCacheIfBadgedFollowers: bad item: %v", err) 328 break 329 } else if typ == keybase1.HomeScreenItemType_PEOPLE { 330 bust = true 331 h.G().Log.CDebugf(ctx, "| in bustHomeCacheIfBadgedFollowers: found badged home people item @%d", i) 332 break 333 } 334 } 335 336 if bust { 337 h.G().Log.CDebugf(ctx, "| busting home cache") 338 h.homeCache = nil 339 } else { 340 h.G().Log.CDebugf(ctx, "| not busting home cache") 341 } 342 343 return nil 344 } 345 346 func (h *Home) SkipTodoType(ctx context.Context, typ keybase1.HomeScreenTodoType) (err error) { 347 var which string 348 var ok bool 349 if which, ok = keybase1.HomeScreenTodoTypeRevMap[typ]; !ok { 350 which = fmt.Sprintf("unknown=%d", int(typ)) 351 } 352 defer h.G().CTrace(ctx, fmt.Sprintf("home#SkipTodoType(%s)", which), &err)() 353 h.bustCache(ctx, false) 354 return h.skipTodoType(ctx, typ) 355 } 356 357 func (h *Home) MarkViewed(ctx context.Context) (err error) { 358 defer h.G().CTrace(ctx, "Home#MarkViewed", &err)() 359 h.Lock() 360 defer h.Unlock() 361 return h.markViewedWithLock(ctx) 362 } 363 364 func (h *Home) markViewedWithLock(ctx context.Context) (err error) { 365 defer h.G().CTrace(ctx, "Home#markViewedWithLock", &err)() 366 err = h.bustHomeCacheIfBadgedFollowers(ctx) 367 if err != nil { 368 return err 369 } 370 return h.markViewedAPICall(ctx) 371 } 372 373 func (h *Home) markViewedAPICall(ctx context.Context) (err error) { 374 mctx := libkb.NewMetaContext(ctx, h.G()) 375 defer mctx.Trace("Home#markViewedAPICall", &err)() 376 377 if _, err = mctx.G().API.Post(mctx, homeRetry(libkb.APIArg{ 378 Endpoint: "home/visit", 379 SessionType: libkb.APISessionTypeREQUIRED, 380 Args: libkb.HTTPArgs{}, 381 })); err != nil { 382 mctx.Warning("Unable to home#markViewedAPICall: %v", err) 383 } 384 return nil 385 } 386 387 func (h *Home) ActionTaken(ctx context.Context) (err error) { 388 defer h.G().CTrace(ctx, "Home#ActionTaken", &err)() 389 h.bustCache(ctx, false) 390 return err 391 } 392 393 func (h *Home) OnLogout(m libkb.MetaContext) error { 394 h.bustCache(m.Ctx(), true) 395 return nil 396 } 397 398 func (h *Home) OnDbNuke(m libkb.MetaContext) error { 399 h.bustCache(m.Ctx(), true) 400 return nil 401 } 402 403 type updateGregorMessage struct { 404 Version int `json:"version"` 405 AnnouncementsVersion int `json:"announcements_version"` 406 } 407 408 func (h *Home) updateUI(ctx context.Context) (err error) { 409 defer h.G().CTrace(ctx, "Home#updateUI", &err)() 410 var ui keybase1.HomeUIInterface 411 if h.G().UIRouter == nil { 412 h.G().Log.CDebugf(ctx, "no UI router, swallowing update") 413 return nil 414 } 415 ui, err = h.G().UIRouter.GetHomeUI() 416 if err != nil { 417 return err 418 } 419 if ui == nil { 420 h.G().Log.CDebugf(ctx, "no registered HomeUI, swallowing update") 421 return nil 422 } 423 err = ui.HomeUIRefresh(context.Background()) 424 return err 425 } 426 427 func (h *Home) handleUpdate(ctx context.Context, item gregor.Item) (err error) { 428 defer h.G().CTrace(ctx, "Home#handleUpdate", &err)() 429 var msg updateGregorMessage 430 if err = json.Unmarshal(item.Body().Bytes(), &msg); err != nil { 431 h.G().Log.Debug("error unmarshaling home.update item: %s", err.Error()) 432 return err 433 } 434 435 h.G().Log.CDebugf(ctx, "home.update unmarshaled: %+v", msg) 436 437 h.handleUpdateWithVersions(ctx, msg.Version, msg.AnnouncementsVersion, true /* send up update UI */) 438 return nil 439 } 440 441 func (h *Home) handleUpdateWithVersions(ctx context.Context, homeVersion int, announcementsVersion int, refreshHome bool) { 442 443 h.Lock() 444 defer func() { 445 if refreshHome { 446 _ = h.updateUI(ctx) 447 } 448 h.Unlock() 449 }() 450 451 if h.homeCache == nil { 452 return 453 } 454 h.G().Log.CDebugf(ctx, "home gregor msg state: (version=%d,announcementsVersion=%d)", h.homeCache.obj.Version, h.homeCache.obj.AnnouncementsVersion) 455 if homeVersion > h.homeCache.obj.Version || announcementsVersion > h.homeCache.obj.AnnouncementsVersion { 456 h.G().Log.CDebugf(ctx, "home gregor msg: clearing cache (new version is <%d,%d>)", homeVersion, announcementsVersion) 457 h.homeCache = nil 458 refreshHome = true 459 } 460 } 461 462 func (h *Home) IsAlive() bool { 463 return true 464 } 465 466 func (h *Home) Name() string { 467 return "Home" 468 } 469 470 func (h *Home) handleUpdateState(ctx context.Context, item gregor.Item) (err error) { 471 defer h.G().CTrace(ctx, "Home#handleUpdateState", &err)() 472 var msg libkb.HomeStateBody 473 if err = json.Unmarshal(item.Body().Bytes(), &msg); err != nil { 474 h.G().Log.Debug("error unmarshaling home.update item: %s", err.Error()) 475 return err 476 } 477 478 h.G().Log.CDebugf(ctx, "home.state unmarshaled: %+v", msg) 479 h.handleUpdateWithVersions(ctx, msg.Version, msg.AnnouncementsVersion, false /* send up update UI */) 480 return nil 481 } 482 483 func (h *Home) Create(ctx context.Context, cli gregor1.IncomingInterface, category string, ibm gregor.Item) (bool, error) { 484 switch category { 485 case "home.update": 486 return true, h.handleUpdate(ctx, ibm) 487 case "home.state": 488 // MK 2020.03.17: This case fixes a race that we observed in the wild, with **announcements**. 489 // The issue is that if you view the home page via home/get and then an announcement is inserted 490 // at roughly the same time. The home/get can then trigger a gregor since the announcement version 491 // bumped. That'll bump the badge state and show a 1 on the home tab. But then when you visit it, 492 // home/get will not be repolled, since there is a fresh home state that just got loaded. You'll have 493 // to wait for 1 hour in which you have a phantom badge. To break this race, we'll clear out the 494 // home state if home.state says the state has moved forward (though we mainly intend that message 495 // to drive badging). 496 // Note that we return false since we still want this message to be handled by other parts 497 // of the system (like the badger). 498 return false, h.handleUpdateState(ctx, ibm) 499 default: 500 if strings.HasPrefix(category, "home.") { 501 return false, fmt.Errorf("unknown home handler category: %q", category) 502 } 503 return false, nil 504 } 505 } 506 507 func (h *Home) Dismiss(ctx context.Context, cli gregor1.IncomingInterface, category string, ibm gregor.Item) (bool, error) { 508 return true, nil 509 } 510 511 type rawPollHome struct { 512 Status libkb.AppStatus `json:"status"` 513 NextPollSecs int `json:"next_poll_secs"` 514 } 515 516 func (r *rawPollHome) GetAppStatus() *libkb.AppStatus { 517 return &r.Status 518 } 519 520 func (h *Home) RunUpdateLoop(m libkb.MetaContext) { 521 go h.updateLoopThread(m) 522 } 523 524 func (h *Home) updateLoopThread(m libkb.MetaContext) { 525 m = m.WithLogTag("HULT") 526 m.Debug("Starting Home#updateLoopThread") 527 slp := time.Minute * (time.Duration(5) + time.Duration((rand.Int() % 10))) 528 var err error 529 for { 530 m.Debug("Sleeping %v until next poll", slp) 531 m.G().Clock().Sleep(slp) 532 slp, err = h.pollOnce(m) 533 if _, ok := err.(libkb.DeviceRequiredError); ok { 534 slp = time.Duration(1) * time.Minute 535 } else if err != nil { 536 slp = time.Duration(15) * time.Minute 537 m.Debug("Hit an error in home update loop: %v", err) 538 } 539 } 540 } 541 542 func (h *Home) pollOnce(m libkb.MetaContext) (d time.Duration, err error) { 543 defer m.Trace("Home#pollOnce", &err)() 544 545 if !m.HasAnySession() { 546 m.Debug("No-op, since don't have keys (and/or am not logged in)") 547 return time.Duration(0), libkb.DeviceRequiredError{} 548 } 549 550 var raw rawPollHome 551 err = m.G().API.GetDecode(m, libkb.APIArg{ 552 Endpoint: "home/poll", 553 SessionType: libkb.APISessionTypeREQUIRED, 554 Args: libkb.HTTPArgs{}, 555 }, &raw) 556 if err != nil { 557 m.Warning("Unable to Home#pollOnce: %v", err) 558 return time.Duration(0), err 559 } 560 return time.Duration(raw.NextPollSecs) * time.Second, nil 561 } 562 563 func findBadUserInUsers(m libkb.MetaContext, l []keybase1.HomeUserSummary, badUIDs map[keybase1.UID]bool) bool { 564 for _, s := range l { 565 if badUIDs[s.Uid] { 566 m.Debug("found blocked uid=%s", s.Uid) 567 return true 568 } 569 } 570 return false 571 } 572 573 func (h *Home) UserBlocked(m libkb.MetaContext, badUIDs map[keybase1.UID]bool) (err error) { 574 h.Lock() 575 defer h.Unlock() 576 577 if !h.peopleCache.hasBadUser(m, badUIDs) { 578 m.Debug("UserBlocked didn't result in any home user suggestions getting blocked, so no-op") 579 return nil 580 } 581 582 h.peopleCache = nil 583 m.Debug("UserBlocked forced home change, updating UI") 584 tmp := h.updateUI(m.Ctx()) 585 if tmp != nil { 586 m.Debug("error updating home UI, but ignoring: %s", tmp) 587 } 588 return nil 589 } 590 591 func (p *peopleCache) hasBadUser(m libkb.MetaContext, badUIDs map[keybase1.UID]bool) bool { 592 if p == nil { 593 m.Debug("nothing to do, people cache is empty") 594 return false 595 } 596 597 if findBadUserInUsers(m, p.all, badUIDs) { 598 m.Debug("Found blocked user in people cache (all)") 599 return true 600 } 601 602 // @maxtaco 2019.11.25: As @jzila points out, there isn't a way for the blocked user to be 603 // in this list, but not the all list above. But let's play it safe and check anyways, 604 // to err on the side of forcing a refresh. 605 if findBadUserInUsers(m, p.lastShown, badUIDs) { 606 m.Debug("Found blocked user in people cache (lastShown)") 607 return true 608 } 609 610 return false 611 }