github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/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  }