1// Copyright 2019 Keybase, Inc. All rights reserved. Use of 2// this source code is governed by the included BSD license. 3 4package contacts 5 6import ( 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 15func 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. 46func 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// 98func ResolveContacts(mctx libkb.MetaContext, provider ContactsProvider, contacts []keybase1.Contact) (res []keybase1.ProcessedContact, err error) { 99 100 if len(contacts) == 0 { 101 mctx.Debug("`contacts` is empty, nothing to resolve") 102 return res, nil 103 } 104 105 // Collect sets of email addresses and phones for provider lookup. Use sets 106 // for deduplication. 107 emailSet := make(map[keybase1.EmailAddress]struct{}) 108 phoneSet := make(map[keybase1.RawPhoneNumber]struct{}) 109 110 for _, contact := range contacts { 111 for _, component := range contact.Components { 112 if component.Email != nil { 113 emailSet[*component.Email] = struct{}{} 114 } 115 if component.PhoneNumber != nil { 116 phoneSet[*component.PhoneNumber] = struct{}{} 117 } 118 } 119 } 120 121 mctx.Debug("Going to look up %d emails and %d phone numbers using provider", len(emailSet), len(phoneSet)) 122 123 actx := externals.MakeStaticAssertionContext(mctx.Ctx()) 124 125 errorComponents := make(map[string]string) 126 userUIDSet := make(map[keybase1.UID]struct{}) 127 128 // Discard duplicate components that come from contacts with the same 129 // contact name and hold the same assertion. Will also skip same assertions 130 // within one contact (duplicated components with same value and same or 131 // different name) 132 type contactAssertionPair struct { 133 contactName string 134 componentValue string 135 } 136 contactAssertionsSeen := make(map[contactAssertionPair]struct{}) 137 138 if len(emailSet)+len(phoneSet) == 0 { 139 // There is nothing to resolve. 140 return res, nil 141 } 142 143 phones := make([]keybase1.RawPhoneNumber, 0, len(phoneSet)) 144 emails := make([]keybase1.EmailAddress, 0, len(emailSet)) 145 for phone := range phoneSet { 146 phones = append(phones, phone) 147 } 148 for email := range emailSet { 149 emails = append(emails, email) 150 } 151 providerRes, err := provider.LookupAll(mctx, emails, phones) 152 if err != nil { 153 return res, err 154 } 155 156 for contactIndex, contact := range contacts { 157 var addLabel = len(contact.Components) > 1 158 for _, component := range contact.Components { 159 assertion, err := AssertionFromComponent(actx, component, "") 160 if err != nil { 161 mctx.Warning("Couldn't make assertion from component: %+v, %q: error: %s", component, component.ValueString(), err) 162 continue 163 } 164 165 cvp := contactAssertionPair{contact.Name, assertion} 166 if _, seen := contactAssertionsSeen[cvp]; seen { 167 // Already seen the exact contact name and assertion. 168 continue 169 } 170 171 if lookupRes, found := providerRes.FindComponent(component); found { 172 if lookupRes.Error != "" { 173 errorComponents[component.ValueString()] = lookupRes.Error 174 mctx.Debug("Could not look up component: %+v, %q, error: %s", component, component.ValueString(), lookupRes.Error) 175 continue 176 } 177 178 if lookupRes.Coerced != "" { 179 // Create assertion again if server gave us coerced version. 180 assertion, err = AssertionFromComponent(actx, component, lookupRes.Coerced) 181 if err != nil { 182 mctx.Warning("Couldn't make assertion from coerced value: %+v, %s: error: %s", component, lookupRes.Coerced, err) 183 continue 184 } 185 } 186 187 res = append(res, keybase1.ProcessedContact{ 188 ContactIndex: contactIndex, 189 ContactName: contact.Name, 190 Component: component, 191 Resolved: true, 192 193 Uid: lookupRes.UID, 194 // Rest of resolved user data is filled by `fillResolvedUserInfo`. 195 196 Assertion: assertion, 197 }) 198 199 userUIDSet[lookupRes.UID] = struct{}{} 200 } else { 201 res = append(res, keybase1.ProcessedContact{ 202 ContactIndex: contactIndex, 203 ContactName: contact.Name, 204 Component: component, 205 Resolved: false, 206 207 DisplayName: contact.Name, 208 DisplayLabel: component.FormatDisplayLabel(addLabel), 209 210 Assertion: assertion, 211 }) 212 } 213 214 // Mark as seen if we got this far. 215 contactAssertionsSeen[cvp] = struct{}{} 216 } 217 } 218 219 mctx.Debug("Got %d contact entries and %d resolved users", len(res), len(userUIDSet)) 220 221 if len(res) > 0 { 222 fillResolvedUserInfo(mctx, provider, userUIDSet, res) 223 224 // And now that we have Keybase names and following information, make a 225 // decision about displayName and displayLabel. 226 for i := range res { 227 v := &res[i] 228 if v.Resolved { 229 v.DisplayName = v.Username 230 switch { 231 case v.Following && v.FullName != "": 232 v.DisplayLabel = v.FullName 233 case v.ContactName != "": 234 v.DisplayLabel = v.ContactName 235 default: 236 v.DisplayLabel = v.Component.ValueString() 237 } 238 } 239 } 240 } 241 242 return res, nil 243} 244