github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/contacts/phonebook.go (about)

     1  package contacts
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/keybase/client/go/encrypteddb"
     8  
     9  	"github.com/keybase/client/go/libkb"
    10  	"github.com/keybase/client/go/protocol/keybase1"
    11  )
    12  
    13  // Saving contact list into encrypted db.
    14  
    15  // Cache resolutions of a lookup ran on entire contact list provided by the
    16  // frontend. Assume every time SaveContacts is called, entire contact list is
    17  // passed as an argument. Always cache the result of last resolution, do not do
    18  // any result merging.
    19  
    20  type SavedContactsStore struct {
    21  	encryptedDB *encrypteddb.EncryptedDB
    22  }
    23  
    24  var _ libkb.SyncedContactListProvider = (*SavedContactsStore)(nil)
    25  
    26  // NewSavedContactsStore creates a new SavedContactsStore for global context.
    27  // The store is used to securely store list of resolved contacts.
    28  func NewSavedContactsStore(g *libkb.GlobalContext) *SavedContactsStore {
    29  	keyFn := func(ctx context.Context) ([32]byte, error) {
    30  		return encrypteddb.GetSecretBoxKey(ctx, g,
    31  			libkb.EncryptionReasonContactsLocalStorage, "encrypting local contact list")
    32  	}
    33  	dbFn := func(g *libkb.GlobalContext) *libkb.JSONLocalDb {
    34  		return g.LocalDb
    35  	}
    36  	return &SavedContactsStore{
    37  		encryptedDB: encrypteddb.New(g, dbFn, keyFn),
    38  	}
    39  }
    40  
    41  func ServiceInit(g *libkb.GlobalContext) {
    42  	g.SyncedContactList = NewSavedContactsStore(g)
    43  }
    44  
    45  func savedContactsDbKey(uid keybase1.UID) libkb.DbKey {
    46  	return libkb.DbKey{
    47  		Typ: libkb.DBSavedContacts,
    48  		Key: fmt.Sprintf("%v", uid),
    49  	}
    50  }
    51  
    52  type savedContactsCache struct {
    53  	Contacts []keybase1.ProcessedContact
    54  	Version  int
    55  }
    56  
    57  const savedContactsCurrentVer = 1
    58  
    59  func assertionToNameDbKey(uid keybase1.UID) libkb.DbKey {
    60  	return libkb.DbKey{
    61  		Typ: libkb.DBSavedContacts,
    62  		Key: fmt.Sprintf("lookup:%v", uid),
    63  	}
    64  }
    65  
    66  type assertionToNameCache struct {
    67  	AssertionToName map[string]string
    68  	Version         int
    69  }
    70  
    71  const assertionToNameCurrentVer = 1
    72  
    73  func ResolveAndSaveContacts(mctx libkb.MetaContext, provider ContactsProvider, contacts []keybase1.Contact) (res keybase1.ContactListResolutionResult, err error) {
    74  	resolveResults, err := ResolveContacts(mctx, provider, contacts)
    75  	if err != nil {
    76  		return res, err
    77  	}
    78  
    79  	// find resolved contacts
    80  	for _, contact := range resolveResults {
    81  		// Strip out the user and anyone they follow.
    82  		if contact.Resolved && !contact.Following &&
    83  			libkb.NewNormalizedUsername(contact.Username) != mctx.CurrentUsername() {
    84  			res.Resolved = append(res.Resolved, contact)
    85  		}
    86  	}
    87  
    88  	// find newly resolved
    89  	s := mctx.G().SyncedContactList
    90  	currentContacts, err := s.RetrieveContacts(mctx)
    91  
    92  	newlyResolvedMap := make(map[string]keybase1.ProcessedContact)
    93  	if err == nil {
    94  		unresolved := make(map[string]bool)
    95  		resolved := make(map[string]bool)
    96  		for _, contact := range currentContacts {
    97  			if contact.Resolved {
    98  				resolved[contact.ContactName] = true
    99  			}
   100  			if resolved[contact.ContactName] {
   101  				// If any contact by this name is resolved, we dedupe.
   102  				delete(unresolved, contact.ContactName)
   103  			} else {
   104  				// We resolve based on display name, not assertion, so we don't
   105  				// duplicate multiple assertions for the same contact.
   106  				unresolved[contact.ContactName] = true
   107  			}
   108  		}
   109  
   110  		for _, resolution := range resolveResults {
   111  			if unresolved[resolution.ContactName] && resolution.Resolved && !resolution.Following {
   112  				// We only want to show one resolution per username.
   113  				newlyResolvedMap[resolution.Username] = resolution
   114  			}
   115  		}
   116  	} else {
   117  		mctx.Warning("error retrieving synced contacts; continuing: %s", err)
   118  	}
   119  
   120  	if len(newlyResolvedMap) == 0 {
   121  		return res, s.SaveProcessedContacts(mctx, resolveResults)
   122  	}
   123  
   124  	resolutionsForPeoplePage := make([]ContactResolution, 0, len(newlyResolvedMap))
   125  	for _, contact := range newlyResolvedMap {
   126  		contactDisplay := contact.ContactName
   127  		if contactDisplay == "" {
   128  			contactDisplay = contact.Component.ValueString()
   129  		}
   130  		resolutionsForPeoplePage = append(resolutionsForPeoplePage, ContactResolution{
   131  			Description: contactDisplay,
   132  			ResolvedUser: keybase1.User{
   133  				Uid:      contact.Uid,
   134  				Username: contact.Username,
   135  			},
   136  		})
   137  		res.NewlyResolved = append(res.NewlyResolved, contact)
   138  	}
   139  	err = SendEncryptedContactResolutionToServer(mctx, resolutionsForPeoplePage)
   140  	if err != nil {
   141  		mctx.Warning("Could not add resolved contacts to people page: %v; returning contacts anyway", err)
   142  	}
   143  
   144  	return res, s.SaveProcessedContacts(mctx, resolveResults)
   145  }
   146  
   147  func makeAssertionToName(contacts []keybase1.ProcessedContact) (res map[string]string) {
   148  	res = make(map[string]string)
   149  	toRemove := make(map[string]struct{})
   150  	for _, contact := range contacts {
   151  		if _, ok := res[contact.Assertion]; ok {
   152  			// multiple contacts match this assertion, remove once we're done
   153  			toRemove[contact.Assertion] = struct{}{}
   154  			continue
   155  		}
   156  		res[contact.Assertion] = contact.ContactName
   157  	}
   158  	for remove := range toRemove {
   159  		delete(res, remove)
   160  	}
   161  	return res
   162  }
   163  
   164  func (s *SavedContactsStore) SaveProcessedContacts(mctx libkb.MetaContext, contacts []keybase1.ProcessedContact) (err error) {
   165  	val := savedContactsCache{
   166  		Contacts: contacts,
   167  		Version:  savedContactsCurrentVer,
   168  	}
   169  
   170  	cacheKey := savedContactsDbKey(mctx.CurrentUID())
   171  	err = s.encryptedDB.Put(mctx.Ctx(), cacheKey, val)
   172  	if err != nil {
   173  		return err
   174  	}
   175  
   176  	assertionToName := makeAssertionToName(contacts)
   177  	lookupVal := assertionToNameCache{
   178  		AssertionToName: assertionToName,
   179  		Version:         assertionToNameCurrentVer,
   180  	}
   181  
   182  	cacheKey = assertionToNameDbKey(mctx.CurrentUID())
   183  	err = s.encryptedDB.Put(mctx.Ctx(), cacheKey, lookupVal)
   184  	return err
   185  }
   186  
   187  func (s *SavedContactsStore) RetrieveContacts(mctx libkb.MetaContext) (ret []keybase1.ProcessedContact, err error) {
   188  	cacheKey := savedContactsDbKey(mctx.CurrentUID())
   189  	var cache savedContactsCache
   190  	found, err := s.encryptedDB.Get(mctx.Ctx(), cacheKey, &cache)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	if !found {
   195  		return ret, nil
   196  	}
   197  	if cache.Version != savedContactsCurrentVer {
   198  		mctx.Warning("synced contact list found but had an old version (found: %d, need: %d), returning empty list",
   199  			cache.Version, savedContactsCurrentVer)
   200  		return ret, nil
   201  	}
   202  	return cache.Contacts, nil
   203  }
   204  
   205  func (s *SavedContactsStore) RetrieveAssertionToName(mctx libkb.MetaContext) (ret map[string]string, err error) {
   206  	cacheKey := assertionToNameDbKey(mctx.CurrentUID())
   207  	var cache assertionToNameCache
   208  	found, err := s.encryptedDB.Get(mctx.Ctx(), cacheKey, &cache)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	if !found {
   213  		return ret, nil
   214  	}
   215  	if cache.Version != assertionToNameCurrentVer {
   216  		mctx.Warning("assertion to name found but had an old version (found: %d, need: %d), returning empty map",
   217  			cache.Version, assertionToNameCurrentVer)
   218  		return ret, nil
   219  	}
   220  	return cache.AssertionToName, nil
   221  }
   222  
   223  func (s *SavedContactsStore) UnresolveContactsWithComponent(mctx libkb.MetaContext,
   224  	phoneNumber *keybase1.PhoneNumber, email *keybase1.EmailAddress) {
   225  	// TODO: Use a phoneNumber | email variant instead of two pointers.
   226  	contactList, err := s.RetrieveContacts(mctx)
   227  	if err != nil {
   228  		mctx.Warning("Failed to get cached contact list: %x", err)
   229  		return
   230  	}
   231  	for i, con := range contactList {
   232  		var unresolve bool
   233  		switch {
   234  		case phoneNumber != nil && con.Component.PhoneNumber != nil:
   235  			unresolve = *con.Component.PhoneNumber == keybase1.RawPhoneNumber(*phoneNumber)
   236  		case email != nil && con.Component.Email != nil:
   237  			unresolve = *con.Component.Email == *email
   238  		}
   239  
   240  		if unresolve {
   241  			// Unresolve contact.
   242  			con.Resolved = false
   243  			con.Username = ""
   244  			con.Uid = ""
   245  			con.Following = false
   246  			con.FullName = ""
   247  			// TODO: DisplayName/DisplayLabel logic infects yet another file /
   248  			// module. But it will sort itself out once we get rid of both.
   249  			con.DisplayName = con.ContactName
   250  			con.DisplayLabel = con.Component.FormatDisplayLabel(false /* addLabel */)
   251  			contactList[i] = con
   252  		}
   253  	}
   254  	err = s.SaveProcessedContacts(mctx, contactList)
   255  	if err != nil {
   256  		mctx.Warning("Failed to put cached contact list: %x", err)
   257  	}
   258  }