github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/badges/badgestate.go (about) 1 // Copyright 2016 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package badges 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "strings" 11 "sync" 12 13 "github.com/keybase/client/go/gregor" 14 "github.com/keybase/client/go/libkb" 15 "github.com/keybase/client/go/logger" 16 "github.com/keybase/client/go/protocol/chat1" 17 "github.com/keybase/client/go/protocol/gregor1" 18 "github.com/keybase/client/go/protocol/keybase1" 19 "github.com/keybase/client/go/protocol/stellar1" 20 jsonw "github.com/keybase/go-jsonw" 21 "golang.org/x/net/context" 22 ) 23 24 type LocalChatState interface { 25 ApplyLocalChatState(context.Context, []keybase1.BadgeConversationInfo) ([]keybase1.BadgeConversationInfo, int, int) 26 } 27 28 type dummyLocalChatState struct{} 29 30 func (d dummyLocalChatState) ApplyLocalChatState(ctx context.Context, i []keybase1.BadgeConversationInfo) ([]keybase1.BadgeConversationInfo, int, int) { 31 return i, 0, 0 32 } 33 34 // BadgeState represents the number of badges on the app. It's threadsafe. 35 // Useable from both the client service and gregor server. 36 // See service:Badger for the service part that owns this. 37 type BadgeState struct { 38 sync.Mutex 39 40 localChatState LocalChatState 41 log logger.Logger 42 env *libkb.Env 43 state keybase1.BadgeState 44 quietLogMode bool 45 46 inboxVers chat1.InboxVers 47 chatUnreadMap map[chat1.ConvIDStr]keybase1.BadgeConversationInfo 48 49 walletUnreadMap map[stellar1.AccountID]int 50 } 51 52 // NewBadgeState creates a new empty BadgeState. 53 func NewBadgeState(log logger.Logger, env *libkb.Env) *BadgeState { 54 return newBadgeState(log, env) 55 } 56 57 // NewBadgeState creates a new empty BadgeState in contexts 58 // where notifications do not need to be handled. 59 func NewBadgeStateForServer(log logger.Logger) *BadgeState { 60 bs := newBadgeState(log, nil) 61 bs.quietLogMode = true 62 return bs 63 } 64 65 func newBadgeState(log logger.Logger, env *libkb.Env) *BadgeState { 66 return &BadgeState{ 67 log: log, 68 env: env, 69 inboxVers: chat1.InboxVers(0), 70 chatUnreadMap: make(map[chat1.ConvIDStr]keybase1.BadgeConversationInfo), 71 walletUnreadMap: make(map[stellar1.AccountID]int), 72 localChatState: dummyLocalChatState{}, 73 quietLogMode: false, 74 } 75 } 76 77 func (b *BadgeState) SetLocalChatState(s LocalChatState) { 78 b.localChatState = s 79 } 80 81 // Exports the state summary 82 func (b *BadgeState) Export(ctx context.Context) (keybase1.BadgeState, error) { 83 b.Lock() 84 defer b.Unlock() 85 86 b.state.Conversations = []keybase1.BadgeConversationInfo{} 87 for _, info := range b.chatUnreadMap { 88 b.state.Conversations = append(b.state.Conversations, info) 89 } 90 b.state.Conversations, b.state.SmallTeamBadgeCount, b.state.BigTeamBadgeCount = 91 b.localChatState.ApplyLocalChatState(ctx, b.state.Conversations) 92 b.state.InboxVers = int(b.inboxVers) 93 94 b.state.UnreadWalletAccounts = []keybase1.WalletAccountInfo{} 95 for accountID, count := range b.walletUnreadMap { 96 info := keybase1.WalletAccountInfo{AccountID: string(accountID), NumUnread: count} 97 b.state.UnreadWalletAccounts = append(b.state.UnreadWalletAccounts, info) 98 } 99 100 return b.state, nil 101 } 102 103 type problemSetBody struct { 104 Count int `json:"count"` 105 } 106 107 type newTeamBody struct { 108 TeamID keybase1.TeamID `json:"id"` 109 TeamName string `json:"name"` 110 Implicit bool `json:"implicit_team"` 111 } 112 113 type teamDeletedBody struct { 114 TeamID string `json:"id"` 115 TeamName string `json:"name"` 116 Implicit bool `json:"implicit_team"` 117 OpBy struct { 118 UID string `json:"uid"` 119 Username string `json:"username"` 120 } `json:"op_by"` 121 } 122 123 type unverifiedCountBody struct { 124 UnverifiedCount int `json:"unverified_count"` 125 } 126 127 // countKnownBadges looks at the map sent down by gregor and considers only those 128 // types that are known to the client. The rest, it assumes it cannot display, 129 // and doesn't count those badges toward the badge count. Note that the shape 130 // of this map is two-deep. 131 // 132 // { 1 : { 2 : 3, 4 : 5 }, 3 : { 10001 : 1 } } 133 // 134 // Implies that are 3 badges on TODO type PROOF, 5 badges on TODO type FOLLOW, 135 // and 1 badges in ANNOUNCEMENTs. 136 func countKnownBadges(m libkb.HomeItemMap) int { 137 var ret int 138 for itemType, todoMap := range m { 139 if _, found := keybase1.HomeScreenItemTypeRevMap[itemType]; !found { 140 continue 141 } 142 for todoType, v := range todoMap { 143 _, found := keybase1.HomeScreenTodoTypeRevMap[todoType] 144 if (itemType == keybase1.HomeScreenItemType_TODO && found) || 145 (itemType == keybase1.HomeScreenItemType_ANNOUNCEMENT && todoType >= keybase1.HomeScreenTodoType_ANNONCEMENT_PLACEHOLDER) { 146 ret += v 147 } 148 } 149 } 150 return ret 151 } 152 153 func (b *BadgeState) ConversationBadgeStr(ctx context.Context, convIDStr chat1.ConvIDStr) int { 154 b.Lock() 155 defer b.Unlock() 156 if info, ok := b.chatUnreadMap[convIDStr]; ok { 157 return info.BadgeCount 158 } 159 return 0 160 } 161 162 func (b *BadgeState) ConversationBadge(ctx context.Context, convID chat1.ConversationID) int { 163 return b.ConversationBadgeStr(ctx, convID.ConvIDStr()) 164 } 165 166 func keyForWotUpdate(w keybase1.WotUpdate) string { 167 return fmt.Sprintf("%s:%s", w.Voucher, w.Vouchee) 168 } 169 170 // UpdateWithGregor updates the badge state from a gregor state. 171 func (b *BadgeState) UpdateWithGregor(ctx context.Context, gstate gregor.State) error { 172 b.Lock() 173 defer b.Unlock() 174 175 b.state.NewTlfs = 0 176 b.state.NewFollowers = 0 177 b.state.RekeysNeeded = 0 178 b.state.NewGitRepoGlobalUniqueIDs = []string{} 179 b.state.NewDevices = []keybase1.DeviceID{} 180 b.state.RevokedDevices = []keybase1.DeviceID{} 181 b.state.NewTeams = nil 182 b.state.DeletedTeams = nil 183 b.state.NewTeamAccessRequestCount = 0 184 b.state.HomeTodoItems = 0 185 b.state.TeamsWithResetUsers = nil 186 b.state.ResetState = keybase1.ResetState{} 187 b.state.UnverifiedEmails = 0 188 b.state.UnverifiedPhones = 0 189 b.state.WotUpdates = make(map[string]keybase1.WotUpdate) 190 191 var hsb *libkb.HomeStateBody 192 193 teamsWithResets := make(map[string]bool) 194 195 items, err := gstate.Items() 196 if err != nil { 197 return err 198 } 199 for _, item := range items { 200 categoryObj := item.Category() 201 if categoryObj == nil { 202 continue 203 } 204 category := categoryObj.String() 205 if strings.HasPrefix(category, "team.request_access:") { 206 b.state.NewTeamAccessRequestCount++ 207 continue 208 } 209 switch category { 210 case "home.state": 211 var tmp libkb.HomeStateBody 212 byt := item.Body().Bytes() 213 dec := json.NewDecoder(bytes.NewReader(byt)) 214 if err := dec.Decode(&tmp); err != nil { 215 b.log.CDebugf(ctx, "BadgeState got bad home.state object; error: %v; on %q", err, string(byt)) 216 continue 217 } 218 sentUp := false 219 if hsb.LessThan(tmp) { 220 hsb = &tmp 221 b.state.HomeTodoItems = countKnownBadges(hsb.BadgeCountMap) 222 sentUp = true 223 } 224 b.log.CDebugf(ctx, "incoming home.state (sentUp=%v): %+v", sentUp, tmp) 225 case "tlf": 226 jsw, err := jsonw.Unmarshal(item.Body().Bytes()) 227 if err != nil { 228 b.log.CDebugf(ctx, "BadgeState encountered non-json 'tlf' item: %v", err) 229 continue 230 } 231 itemType, err := jsw.AtKey("type").GetString() 232 if err != nil { 233 b.log.CDebugf(ctx, "BadgeState encountered gregor 'tlf' item without 'type': %v", err) 234 continue 235 } 236 if itemType != "created" { 237 continue 238 } 239 b.state.NewTlfs++ 240 case "kbfs_tlf_problem_set_count", "kbfs_tlf_sbs_problem_set_count": 241 var body problemSetBody 242 if err := json.Unmarshal(item.Body().Bytes(), &body); err != nil { 243 b.log.CDebugf(ctx, "BadgeState encountered non-json 'problem set' item: %v", err) 244 continue 245 } 246 b.state.RekeysNeeded += body.Count 247 case "follow": 248 b.state.NewFollowers++ 249 case "device.new": 250 jsw, err := jsonw.Unmarshal(item.Body().Bytes()) 251 if err != nil { 252 b.log.CDebugf(ctx, "BadgeState encountered non-json 'device.new' item: %v", err) 253 continue 254 } 255 newDeviceID, err := jsw.AtKey("device_id").GetString() 256 if err != nil { 257 b.log.CDebugf(ctx, "BadgeState encountered gregor 'device.new' item without 'device_id': %v", err) 258 continue 259 } 260 b.state.NewDevices = append(b.state.NewDevices, keybase1.DeviceID(newDeviceID)) 261 case "wot.new_vouch": 262 jsw, err := jsonw.Unmarshal(item.Body().Bytes()) 263 if err != nil { 264 b.log.CDebugf(ctx, "BadgeState encountered non-json 'wot.new_vouch' item: %v", err) 265 continue 266 } 267 voucher, err := jsw.AtKey("voucher").GetString() 268 if err != nil { 269 b.log.CDebugf(ctx, "BadgeState encountered gregor 'wot.new_vouch' item without 'voucherUid': %v", err) 270 continue 271 } 272 vouchee := b.env.GetUsername().String() 273 wotUpdate := keybase1.WotUpdate{ 274 Voucher: voucher, 275 Vouchee: vouchee, 276 Status: keybase1.WotStatusType_PROPOSED, 277 } 278 b.state.WotUpdates[keyForWotUpdate(wotUpdate)] = wotUpdate 279 case "wot.accepted", "wot.rejected": 280 jsw, err := jsonw.Unmarshal(item.Body().Bytes()) 281 if err != nil { 282 b.log.CDebugf(ctx, "BadgeState encountered non-json '%s' item: %v", category, err) 283 continue 284 } 285 vouchee, err := jsw.AtKey("vouchee").GetString() 286 if err != nil { 287 b.log.CDebugf(ctx, "BadgeState encountered gregor '%s' item without 'voucherUid': %v", category, err) 288 continue 289 } 290 status := keybase1.WotStatusType_ACCEPTED 291 if category == "wot.rejected" { 292 status = keybase1.WotStatusType_REJECTED 293 } 294 voucher := b.env.GetUsername().String() 295 wotUpdate := keybase1.WotUpdate{ 296 Voucher: voucher, 297 Vouchee: vouchee, 298 Status: status, 299 } 300 b.state.WotUpdates[keyForWotUpdate(wotUpdate)] = wotUpdate 301 case "device.revoked": 302 jsw, err := jsonw.Unmarshal(item.Body().Bytes()) 303 if err != nil { 304 b.log.CDebugf(ctx, "BadgeState encountered non-json 'device.revoked' item: %v", err) 305 continue 306 } 307 revokedDeviceID, err := jsw.AtKey("device_id").GetString() 308 if err != nil { 309 b.log.CDebugf(ctx, "BadgeState encountered gregor 'device.revoked' item without 'device_id': %v", err) 310 continue 311 } 312 b.state.RevokedDevices = append(b.state.RevokedDevices, keybase1.DeviceID(revokedDeviceID)) 313 case "new_git_repo": 314 jsw, err := jsonw.Unmarshal(item.Body().Bytes()) 315 if err != nil { 316 b.log.CDebugf(ctx, "BadgeState encountered non-json 'new_git_repo' item: %v", err) 317 continue 318 } 319 globalUniqueID, err := jsw.AtKey("global_unique_id").GetString() 320 if err != nil { 321 b.log.CDebugf(ctx, 322 "BadgeState encountered gregor 'new_git_repo' item without 'global_unique_id': %v", err) 323 continue 324 } 325 b.state.NewGitRepoGlobalUniqueIDs = append(b.state.NewGitRepoGlobalUniqueIDs, globalUniqueID) 326 case "team.newly_added_to_team": 327 var body []newTeamBody 328 if err := json.Unmarshal(item.Body().Bytes(), &body); err != nil { 329 b.log.CDebugf(ctx, "BadgeState unmarshal error for team.newly_added_to_team item: %v", err) 330 continue 331 } 332 for _, x := range body { 333 if x.TeamName == "" { 334 continue 335 } 336 if x.Implicit { 337 continue 338 } 339 b.state.NewTeams = append(b.state.NewTeams, x.TeamID) 340 } 341 case "team.delete": 342 var body []teamDeletedBody 343 if err := json.Unmarshal(item.Body().Bytes(), &body); err != nil { 344 b.log.CDebugf(ctx, "BadgeState unmarshal error for team.delete item: %v", err) 345 continue 346 } 347 348 msgID := item.Metadata().MsgID().(gregor1.MsgID) 349 var username string 350 if b.env != nil { 351 username = b.env.GetUsername().String() 352 } 353 for _, x := range body { 354 if x.TeamName == "" || x.OpBy.Username == "" || x.OpBy.Username == username { 355 continue 356 } 357 if x.Implicit { 358 continue 359 } 360 b.state.DeletedTeams = append(b.state.DeletedTeams, keybase1.DeletedTeamInfo{ 361 TeamName: x.TeamName, 362 DeletedBy: x.OpBy.Username, 363 Id: msgID, 364 }) 365 } 366 case "team.member_out_from_reset": 367 var body keybase1.TeamMemberOutFromReset 368 if err := json.Unmarshal(item.Body().Bytes(), &body); err != nil { 369 b.log.CDebugf(ctx, "BadgeState unmarshal error for team.member_out_from_reset item: %v", err) 370 continue 371 } 372 373 if body.ResetUser.IsDelete { 374 b.log.CDebugf(ctx, "BadgeState ignoring member_out_from_reset for deleted user") 375 continue 376 } 377 378 msgID := item.Metadata().MsgID().(gregor1.MsgID) 379 m := keybase1.TeamMemberOutReset{ 380 TeamID: body.TeamID, 381 Teamname: body.TeamName, 382 Uid: body.ResetUser.Uid, 383 Username: body.ResetUser.Username, 384 Id: msgID, 385 } 386 387 key := m.Teamname + "|" + m.Username 388 if !teamsWithResets[key] { 389 b.state.TeamsWithResetUsers = append(b.state.TeamsWithResetUsers, m) 390 teamsWithResets[key] = true 391 } 392 case "autoreset": 393 var body keybase1.ResetState 394 if err := json.Unmarshal(item.Body().Bytes(), &body); err != nil { 395 b.log.CDebugf(ctx, "BadgeState encountered non-json 'autoreset' item: %v", err) 396 continue 397 } 398 b.state.ResetState = body 399 case "email.unverified_count": 400 var body unverifiedCountBody 401 if err := json.Unmarshal(item.Body().Bytes(), &body); err != nil { 402 b.log.CDebugf(ctx, "BadgeState encountered non-json 'email.unverified_count' item: %v", err) 403 continue 404 } 405 b.state.UnverifiedEmails = body.UnverifiedCount 406 case "phone.unverified_count": 407 var body unverifiedCountBody 408 if err := json.Unmarshal(item.Body().Bytes(), &body); err != nil { 409 b.log.CDebugf(ctx, "BadgeState encountered non-json 'phone.unverified_count' item: %v", err) 410 continue 411 } 412 b.state.UnverifiedPhones = body.UnverifiedCount 413 } 414 } 415 416 return nil 417 } 418 419 func (b *BadgeState) UpdateWithChat(ctx context.Context, update chat1.UnreadUpdate, 420 inboxVers chat1.InboxVers, isMobile bool) { 421 b.Lock() 422 defer b.Unlock() 423 424 // Skip stale updates 425 if inboxVers < b.inboxVers { 426 return 427 } 428 429 b.inboxVers = inboxVers 430 b.updateWithChat(ctx, update, isMobile) 431 } 432 433 func (b *BadgeState) UpdateWithChatFull(ctx context.Context, update chat1.UnreadUpdateFull, isMobile bool) { 434 b.Lock() 435 defer b.Unlock() 436 437 if update.Ignore { 438 return 439 } 440 441 // Skip stale updates 442 if update.InboxVers < b.inboxVers { 443 return 444 } 445 446 switch update.InboxSyncStatus { 447 case chat1.SyncInboxResType_CURRENT: 448 case chat1.SyncInboxResType_INCREMENTAL: 449 case chat1.SyncInboxResType_CLEAR: 450 b.chatUnreadMap = make(map[chat1.ConvIDStr]keybase1.BadgeConversationInfo) 451 } 452 453 for _, upd := range update.Updates { 454 b.updateWithChat(ctx, upd, isMobile) 455 } 456 457 b.inboxVers = update.InboxVers 458 } 459 460 func (b *BadgeState) Clear() { 461 b.Lock() 462 defer b.Unlock() 463 464 b.state = keybase1.BadgeState{} 465 b.inboxVers = chat1.InboxVers(0) 466 b.chatUnreadMap = make(map[chat1.ConvIDStr]keybase1.BadgeConversationInfo) 467 b.walletUnreadMap = make(map[stellar1.AccountID]int) 468 } 469 470 func (b *BadgeState) updateWithChat(ctx context.Context, update chat1.UnreadUpdate, isMobile bool) { 471 if !b.quietLogMode { 472 b.log.CDebugf(ctx, "updateWithChat: %s", update) 473 } 474 deviceType := keybase1.DeviceType_DESKTOP 475 if isMobile { 476 deviceType = keybase1.DeviceType_MOBILE 477 } 478 if update.Diff { 479 cur := b.chatUnreadMap[update.ConvID.ConvIDStr()] 480 cur.ConvID = keybase1.ChatConversationID(update.ConvID) 481 cur.UnreadMessages += update.UnreadMessages 482 cur.BadgeCount += update.UnreadNotifyingMessages[deviceType] 483 b.chatUnreadMap[update.ConvID.ConvIDStr()] = cur 484 } else { 485 b.chatUnreadMap[update.ConvID.ConvIDStr()] = keybase1.BadgeConversationInfo{ 486 ConvID: keybase1.ChatConversationID(update.ConvID), 487 UnreadMessages: update.UnreadMessages, 488 BadgeCount: update.UnreadNotifyingMessages[deviceType], 489 } 490 } 491 } 492 493 // SetWalletAccountUnreadCount sets the unread count for a wallet account. 494 // It returns true if the call changed the unread count for accountID. 495 func (b *BadgeState) SetWalletAccountUnreadCount(accountID stellar1.AccountID, unreadCount int) bool { 496 b.Lock() 497 existingCount := b.walletUnreadMap[accountID] 498 b.walletUnreadMap[accountID] = unreadCount 499 b.Unlock() 500 501 // did this call change the unread count for this accountID? 502 changed := unreadCount != existingCount 503 504 return changed 505 }