github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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 }