github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/contacts/contacts.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 contacts
     5  
     6  import (
     7  	"errors"
     8  	"strings"
     9  
    10  	"github.com/keybase/client/go/externals"
    11  	"github.com/keybase/client/go/libkb"
    12  	"github.com/keybase/client/go/protocol/keybase1"
    13  )
    14  
    15  func AssertionFromComponent(actx libkb.AssertionContext, c keybase1.ContactComponent, coercedValue string) (string, error) {
    16  	key := c.AssertionType()
    17  	var value string
    18  	if coercedValue != "" {
    19  		value = coercedValue
    20  	} else {
    21  		value = c.ValueString()
    22  	}
    23  	if key == "phone" {
    24  		// ContactComponent has the PhoneNumber type which is E164 phone
    25  		// number starting with `+`, we need to remove all non-digits for
    26  		// the assertion.
    27  		value = keybase1.PhoneNumberToAssertionValue(value)
    28  	} else {
    29  		value = strings.ToLower(strings.TrimSpace(value))
    30  	}
    31  	if key == "" || value == "" {
    32  		return "", errors.New("invalid variant value in contact component")
    33  	}
    34  	ret, err := libkb.ParseAssertionURLKeyValue(actx, key, value, true /* strict */)
    35  	if err != nil {
    36  		return "", err
    37  	}
    38  	return ret.String(), nil
    39  }
    40  
    41  // fillResolvedUserInfo takes uidSet and processed contact list and fill the
    42  // following info (in place) for resolved contacts:
    43  // - usernames and full names,
    44  // - follow status (are we following the user or not),
    45  // - service summaries.
    46  func fillResolvedUserInfo(mctx libkb.MetaContext, provider ContactsProvider, uidSet map[keybase1.UID]struct{},
    47  	contacts []keybase1.ProcessedContact) {
    48  
    49  	uidList := make([]keybase1.UID, 0, len(uidSet))
    50  	for uid := range uidSet {
    51  		uidList = append(uidList, uid)
    52  	}
    53  
    54  	// Uidmap everything to get Keybase usernames and full names.
    55  	usernames, err := provider.FindUsernames(mctx, uidList)
    56  	if err != nil {
    57  		mctx.Warning("Unable to find usernames for contacts: %s", err)
    58  		usernames = make(map[keybase1.UID]ContactUsernameAndFullName)
    59  	}
    60  
    61  	// Get tracking info and set "Following" field for contacts.
    62  	following, err := provider.FindFollowing(mctx, uidList)
    63  	if err != nil {
    64  		mctx.Warning("Unable to find tracking info for contacts: %s", err)
    65  		following = make(map[keybase1.UID]bool)
    66  	}
    67  
    68  	// Get service maps
    69  	serviceMaps, err := provider.FindServiceMaps(mctx, uidList)
    70  	if err != nil {
    71  		mctx.Warning("Unable to get service maps for contacts: %s", err)
    72  		serviceMaps = make(map[keybase1.UID]libkb.UserServiceSummary)
    73  	}
    74  
    75  	for i := range contacts {
    76  		v := &contacts[i]
    77  		if v.Resolved {
    78  			if unamePkg, found := usernames[v.Uid]; found {
    79  				v.Username = unamePkg.Username
    80  				v.FullName = unamePkg.Fullname
    81  			}
    82  			if follow, found := following[v.Uid]; found {
    83  				v.Following = follow
    84  			}
    85  			if smap, found := serviceMaps[v.Uid]; found && len(smap) > 0 {
    86  				v.ServiceMap = make(map[string]string, len(smap))
    87  				for service, username := range smap {
    88  					v.ServiceMap[service] = username
    89  				}
    90  			}
    91  		}
    92  	}
    93  }
    94  
    95  // ResolveContacts resolves contacts with cache for UI. See API documentation
    96  // in phone_numbers.avdl
    97  func ResolveContacts(mctx libkb.MetaContext, provider ContactsProvider, contacts []keybase1.Contact) (res []keybase1.ProcessedContact, err error) {
    98  
    99  	if len(contacts) == 0 {
   100  		mctx.Debug("`contacts` is empty, nothing to resolve")
   101  		return res, nil
   102  	}
   103  
   104  	// Collect sets of email addresses and phones for provider lookup. Use sets
   105  	// for deduplication.
   106  	emailSet := make(map[keybase1.EmailAddress]struct{})
   107  	phoneSet := make(map[keybase1.RawPhoneNumber]struct{})
   108  
   109  	for _, contact := range contacts {
   110  		for _, component := range contact.Components {
   111  			if component.Email != nil {
   112  				emailSet[*component.Email] = struct{}{}
   113  			}
   114  			if component.PhoneNumber != nil {
   115  				phoneSet[*component.PhoneNumber] = struct{}{}
   116  			}
   117  		}
   118  	}
   119  
   120  	mctx.Debug("Going to look up %d emails and %d phone numbers using provider", len(emailSet), len(phoneSet))
   121  
   122  	actx := externals.MakeStaticAssertionContext(mctx.Ctx())
   123  
   124  	errorComponents := make(map[string]string)
   125  	userUIDSet := make(map[keybase1.UID]struct{})
   126  
   127  	// Discard duplicate components that come from contacts with the same
   128  	// contact name and hold the same assertion. Will also skip same assertions
   129  	// within one contact (duplicated components with same value and same or
   130  	// different name)
   131  	type contactAssertionPair struct {
   132  		contactName    string
   133  		componentValue string
   134  	}
   135  	contactAssertionsSeen := make(map[contactAssertionPair]struct{})
   136  
   137  	if len(emailSet)+len(phoneSet) == 0 {
   138  		// There is nothing to resolve.
   139  		return res, nil
   140  	}
   141  
   142  	phones := make([]keybase1.RawPhoneNumber, 0, len(phoneSet))
   143  	emails := make([]keybase1.EmailAddress, 0, len(emailSet))
   144  	for phone := range phoneSet {
   145  		phones = append(phones, phone)
   146  	}
   147  	for email := range emailSet {
   148  		emails = append(emails, email)
   149  	}
   150  	providerRes, err := provider.LookupAll(mctx, emails, phones)
   151  	if err != nil {
   152  		return res, err
   153  	}
   154  
   155  	for contactIndex, contact := range contacts {
   156  		var addLabel = len(contact.Components) > 1
   157  		for _, component := range contact.Components {
   158  			assertion, err := AssertionFromComponent(actx, component, "")
   159  			if err != nil {
   160  				mctx.Warning("Couldn't make assertion from component: %+v, %q: error: %s", component, component.ValueString(), err)
   161  				continue
   162  			}
   163  
   164  			cvp := contactAssertionPair{contact.Name, assertion}
   165  			if _, seen := contactAssertionsSeen[cvp]; seen {
   166  				// Already seen the exact contact name and assertion.
   167  				continue
   168  			}
   169  
   170  			if lookupRes, found := providerRes.FindComponent(component); found {
   171  				if lookupRes.Error != "" {
   172  					errorComponents[component.ValueString()] = lookupRes.Error
   173  					mctx.Debug("Could not look up component: %+v, %q, error: %s", component, component.ValueString(), lookupRes.Error)
   174  					continue
   175  				}
   176  
   177  				if lookupRes.Coerced != "" {
   178  					// Create assertion again if server gave us coerced version.
   179  					assertion, err = AssertionFromComponent(actx, component, lookupRes.Coerced)
   180  					if err != nil {
   181  						mctx.Warning("Couldn't make assertion from coerced value: %+v, %s: error: %s", component, lookupRes.Coerced, err)
   182  						continue
   183  					}
   184  				}
   185  
   186  				res = append(res, keybase1.ProcessedContact{
   187  					ContactIndex: contactIndex,
   188  					ContactName:  contact.Name,
   189  					Component:    component,
   190  					Resolved:     true,
   191  
   192  					Uid: lookupRes.UID,
   193  					// Rest of resolved user data is filled by `fillResolvedUserInfo`.
   194  
   195  					Assertion: assertion,
   196  				})
   197  
   198  				userUIDSet[lookupRes.UID] = struct{}{}
   199  			} else {
   200  				res = append(res, keybase1.ProcessedContact{
   201  					ContactIndex: contactIndex,
   202  					ContactName:  contact.Name,
   203  					Component:    component,
   204  					Resolved:     false,
   205  
   206  					DisplayName:  contact.Name,
   207  					DisplayLabel: component.FormatDisplayLabel(addLabel),
   208  
   209  					Assertion: assertion,
   210  				})
   211  			}
   212  
   213  			// Mark as seen if we got this far.
   214  			contactAssertionsSeen[cvp] = struct{}{}
   215  		}
   216  	}
   217  
   218  	mctx.Debug("Got %d contact entries and %d resolved users", len(res), len(userUIDSet))
   219  
   220  	if len(res) > 0 {
   221  		fillResolvedUserInfo(mctx, provider, userUIDSet, res)
   222  
   223  		// And now that we have Keybase names and following information, make a
   224  		// decision about displayName and displayLabel.
   225  		for i := range res {
   226  			v := &res[i]
   227  			if v.Resolved {
   228  				v.DisplayName = v.Username
   229  				switch {
   230  				case v.Following && v.FullName != "":
   231  					v.DisplayLabel = v.FullName
   232  				case v.ContactName != "":
   233  					v.DisplayLabel = v.ContactName
   234  				default:
   235  					v.DisplayLabel = v.Component.ValueString()
   236  				}
   237  			}
   238  		}
   239  	}
   240  
   241  	return res, nil
   242  }