github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/service/usersearch.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 service 5 6 import ( 7 "fmt" 8 "regexp" 9 "sort" 10 "strings" 11 12 "github.com/keybase/client/go/contacts" 13 email_utils "github.com/keybase/client/go/emails" 14 "github.com/keybase/client/go/externals" 15 "github.com/keybase/client/go/libkb" 16 keybase1 "github.com/keybase/client/go/protocol/keybase1" 17 "github.com/keybase/go-framed-msgpack-rpc/rpc" 18 "golang.org/x/net/context" 19 20 "golang.org/x/text/cases" 21 "golang.org/x/text/language" 22 "golang.org/x/text/unicode/norm" 23 ) 24 25 type UserSearchProvider interface { 26 MakeSearchRequest(libkb.MetaContext, keybase1.UserSearchArg) ([]keybase1.APIUserSearchResult, error) 27 } 28 29 type UserSearchHandler struct { 30 libkb.Contextified 31 *BaseHandler 32 33 contactsProvider *contacts.CachedContactsProvider 34 // Tests can overwrite searchProvider with mock types. 35 searchProvider UserSearchProvider 36 } 37 38 func NewUserSearchHandler(xp rpc.Transporter, g *libkb.GlobalContext, provider *contacts.CachedContactsProvider) *UserSearchHandler { 39 handler := &UserSearchHandler{ 40 Contextified: libkb.NewContextified(g), 41 BaseHandler: NewBaseHandler(g, xp), 42 contactsProvider: provider, 43 searchProvider: &KeybaseAPISearchProvider{}, 44 } 45 return handler 46 } 47 48 var _ keybase1.UserSearchInterface = (*UserSearchHandler)(nil) 49 50 type rawSearchResults struct { 51 libkb.AppStatusEmbed 52 List []keybase1.APIUserSearchResult `json:"list"` 53 } 54 55 type KeybaseAPISearchProvider struct{} 56 57 func (*KeybaseAPISearchProvider) MakeSearchRequest(mctx libkb.MetaContext, arg keybase1.UserSearchArg) (res []keybase1.APIUserSearchResult, err error) { 58 service := arg.Service 59 if service == "keybase" { 60 service = "" 61 } 62 apiArg := libkb.APIArg{ 63 Endpoint: "user/user_search", 64 SessionType: libkb.APISessionTypeOPTIONAL, 65 Args: libkb.HTTPArgs{ 66 "q": libkb.S{Val: arg.Query}, 67 "num_wanted": libkb.I{Val: arg.MaxResults}, 68 "service": libkb.S{Val: service}, 69 "include_services_summary": libkb.B{Val: arg.IncludeServicesSummary}, 70 }, 71 } 72 var response rawSearchResults 73 err = mctx.G().API.GetDecode(mctx, apiArg, &response) 74 if err != nil { 75 return nil, err 76 } 77 return response.List, nil 78 } 79 80 func normalizeText(str string) string { 81 return strings.ToLower(string(norm.NFKD.Bytes([]byte(str)))) 82 } 83 84 var splitRxx = regexp.MustCompile(`[-\s!$%^&*()_+|~=` + "`" + `{}\[\]:";'<>?,.\/]+`) 85 86 func queryToRegexp(q string) (*regexp.Regexp, error) { 87 parts := splitRxx.Split(q, -1) 88 nonEmptyParts := make([]string, 0, len(parts)) 89 for _, p := range parts { 90 if p != "" { 91 nonEmptyParts = append(nonEmptyParts, p) 92 } 93 } 94 rxx, err := regexp.Compile(strings.Join(nonEmptyParts, ".*")) 95 if err != nil { 96 return nil, err 97 } 98 rxx.Longest() 99 return rxx, nil 100 } 101 102 type compiledQuery struct { 103 query string 104 rxx *regexp.Regexp 105 } 106 107 func compileQuery(query string) (res compiledQuery, err error) { 108 query = normalizeText(query) 109 rxx, err := queryToRegexp(query) 110 if err != nil { 111 return res, err 112 } 113 res = compiledQuery{ 114 query: query, 115 rxx: rxx, 116 } 117 return res, nil 118 } 119 120 func (q *compiledQuery) scoreString(str string) (bool, float64) { 121 norm := normalizeText(str) 122 if norm == q.query { 123 return true, 1 124 } 125 126 index := q.rxx.FindStringIndex(norm) 127 if index == nil { 128 return false, 0 129 } 130 131 leadingScore := 1.0 / float64(1+index[0]) 132 lengthScore := 1.0 / float64(1+len(norm)) 133 imperfection := 0.5 134 score := leadingScore * lengthScore * imperfection 135 return true, score 136 } 137 138 var fieldsAndScores = []struct { 139 multiplier float64 140 plumb bool // plumb the matched value to displayLabel 141 getter func(*keybase1.ProcessedContact) string 142 }{ 143 {1.5, true, func(contact *keybase1.ProcessedContact) string { return contact.ContactName }}, 144 {1.0, true, func(contact *keybase1.ProcessedContact) string { return contact.Component.ValueString() }}, 145 {1.0, false, func(contact *keybase1.ProcessedContact) string { return contact.DisplayName }}, 146 {0.8, false, func(contact *keybase1.ProcessedContact) string { return contact.DisplayLabel }}, 147 {0.7, false, func(contact *keybase1.ProcessedContact) string { return contact.FullName }}, 148 {0.7, false, func(contact *keybase1.ProcessedContact) string { return contact.Username }}, 149 } 150 151 func matchAndScoreContact(query compiledQuery, contact keybase1.ProcessedContact) (found bool, score float64, plumbMatchedVal string) { 152 var currentScore float64 153 var multiplier float64 154 for _, v := range fieldsAndScores { 155 str := v.getter(&contact) 156 if str == "" { 157 continue 158 } 159 matchFound, matchScore := query.scoreString(str) 160 if matchFound && matchScore > currentScore { 161 plumbMatchedVal = "" 162 if v.plumb { 163 plumbMatchedVal = str 164 } 165 found = true 166 currentScore = matchScore 167 multiplier = v.multiplier 168 } 169 170 } 171 return found, currentScore * multiplier, plumbMatchedVal 172 } 173 174 func compareUserSearch(i, j keybase1.APIUserSearchResult) bool { 175 // Float comparasion - we expect exact floats here when multiple 176 // results match in same way and yield identical score thorugh 177 // same scoring operations. 178 if i.RawScore == j.RawScore { 179 idI := i.GetStringIDForCompare() 180 idJ := j.GetStringIDForCompare() 181 return idI > idJ 182 } 183 return i.RawScore > j.RawScore 184 } 185 186 func contactSearch(mctx libkb.MetaContext, arg keybase1.UserSearchArg) (res []keybase1.APIUserSearchResult, err error) { 187 contactsRes, err := mctx.G().SyncedContactList.RetrieveContacts(mctx) 188 if err != nil { 189 return res, err 190 } 191 192 query, err := compileQuery(arg.Query) 193 if err != nil { 194 return res, nil 195 } 196 197 // Deduplicate on name and label - never return multiple identical rows 198 // even if separate components yielded them. 199 type displayNameAndLabel struct { 200 name, label string 201 } 202 searchResults := make(map[displayNameAndLabel]keybase1.APIUserSearchResult) 203 204 // Set of contact indices that we've matched to and are resolved. When 205 // search matches to a contact component, we want to only present the 206 // resolved one and skip unresolved. 207 seenResolvedContacts := make(map[int]struct{}) 208 209 for _, contactIter := range contactsRes { 210 found, score, matchedVal := matchAndScoreContact(query, contactIter) 211 if found { 212 // Copy contact because we are storing pointer to contact. 213 contact := contactIter 214 if contact.Resolved { 215 if matchedVal != "" { 216 // If contact is resolved, make sure to plumb matched query to 217 // display label. This is not needed for unresolved contacts, 218 // which can only match on ContactName or Component Value, and 219 // both of them always appear as name and label. 220 contact.DisplayLabel = matchedVal 221 } 222 223 // If we got a resolved match, add bonus to the score so it 224 // stands out from similar matches. 225 score *= 1.5 226 227 // Mark contact index so we skip it when populating return list. 228 seenResolvedContacts[contact.ContactIndex] = struct{}{} 229 } else { 230 if _, seen := seenResolvedContacts[contact.ContactIndex]; seen { 231 // Other component of this contact has resolved to a user, skip 232 // all non-resolved components. 233 continue 234 } 235 if contact.Component.PhoneNumber != nil { 236 // Phone numbers are better, "mobile" phone numbers are best. 237 // This is for sorting matches within one contact (for which we expect 238 // the scores to be equal), so only increase by a small amount. 239 score *= 1.01 240 if contact.Component.Label == "mobile" { 241 score *= 1.01 242 } 243 } 244 } 245 246 key := displayNameAndLabel{contact.DisplayName, contact.DisplayLabel} 247 replace := true 248 if current, found := searchResults[key]; found { 249 replace = (contact.Resolved && !current.Contact.Resolved) || (score > current.RawScore) 250 } 251 252 if replace { 253 searchResults[key] = keybase1.APIUserSearchResult{ 254 Contact: &contact, 255 RawScore: score, 256 } 257 } 258 } 259 } 260 261 for _, entry := range searchResults { 262 if !entry.Contact.Resolved { 263 if _, seen := seenResolvedContacts[entry.Contact.ContactIndex]; seen { 264 // Other component of this contact has resolved to a user, skip 265 // all non-resolved components. 266 continue 267 } 268 } 269 270 res = append(res, entry) 271 } 272 273 // Return best matches first. 274 sort.Slice(res, func(i, j int) bool { 275 return compareUserSearch(res[i], res[j]) 276 }) 277 278 // Trim to maxResults to reduce complexity on the call site. 279 maxRes := arg.MaxResults 280 if maxRes > 0 && len(res) > maxRes { 281 res = res[:maxRes] 282 } 283 284 return res, nil 285 } 286 287 func imptofuQueryToAssertion(ctx context.Context, typ, val string) (ret libkb.AssertionURL, err error) { 288 parsef := func(key, val string) (libkb.AssertionURL, error) { 289 return libkb.ParseAssertionURLKeyValue( 290 externals.MakeStaticAssertionContext(ctx), key, val, true) 291 } 292 switch typ { 293 case "phone": 294 ret, err = parsef("phone", keybase1.PhoneNumberToAssertionValue(val)) 295 case "email": 296 ret, err = parsef("email", strings.ToLower(strings.TrimSpace(val))) 297 default: 298 err = fmt.Errorf("invalid assertion type for imptofuQueryToAssertion, got %q", typ) 299 } 300 return ret, err 301 } 302 303 // Search result coming from `searchEmailsOrPhoneNumbers`. 304 type imptofuQueryResult struct { 305 input string 306 validInput bool 307 assertion libkb.AssertionURL 308 309 found bool 310 UID keybase1.UID 311 username string 312 fullName string 313 serviceMap libkb.UserServiceSummary 314 } 315 316 type searchEmailsOrPhoneNumbersResult struct { 317 emails []imptofuQueryResult 318 phoneNumbers []imptofuQueryResult 319 } 320 321 func (h *UserSearchHandler) searchEmailsOrPhoneNumbers(mctx libkb.MetaContext, emails []keybase1.EmailAddress, 322 phoneNumbers []keybase1.RawPhoneNumber, requireUsernames bool, 323 includeServicesSummary bool) (ret searchEmailsOrPhoneNumbersResult, err error) { 324 325 // Create assertions from e-mail addresses. Only search for the ones that 326 // actually yield a valid assertions, but return all of them in results 327 // from this function. 328 emailsToSearch := make([]keybase1.EmailAddress, 0, len(emails)) 329 emailRes := make([]imptofuQueryResult, len(emails)) 330 for i, email := range emails { 331 emailRes[i].input = string(email) 332 assertion, err := imptofuQueryToAssertion(mctx.Ctx(), "email", string(email)) 333 if err == nil { 334 emailRes[i].validInput = true 335 emailRes[i].assertion = assertion 336 emailsToSearch = append(emailsToSearch, email) 337 } else { 338 mctx.Debug("Failed to create assertion from email: %s, skipping in search", email) 339 } 340 } 341 342 phonesToSearch := make([]keybase1.RawPhoneNumber, 0, len(phoneNumbers)) 343 phoneRes := make([]imptofuQueryResult, len(phoneNumbers)) 344 for i, phone := range phoneNumbers { 345 phoneRes[i].input = string(phone) 346 assertion, err := imptofuQueryToAssertion(mctx.Ctx(), "phone", string(phone)) 347 if err == nil { 348 phoneRes[i].validInput = true 349 phoneRes[i].assertion = assertion 350 phonesToSearch = append(phonesToSearch, phone) 351 } else { 352 mctx.Debug("Failed to create assertion from phone number: %s, skipping in search", phone) 353 } 354 } 355 356 ret.emails = emailRes 357 ret.phoneNumbers = phoneRes 358 359 if len(emailsToSearch)+len(phonesToSearch) == 0 { 360 // Everything was invalid (or we were given two empty lists). 361 return ret, nil 362 } 363 364 lookupRes, err := h.contactsProvider.LookupAll(mctx, emailsToSearch, phonesToSearch) 365 if err != nil { 366 return ret, err 367 } 368 if len(lookupRes.Results) == 0 { 369 return ret, nil 370 } 371 372 uids := make([]keybase1.UID, 0, len(lookupRes.Results)) 373 for _, v := range lookupRes.Results { 374 if v.Error == "" && v.UID.Exists() { 375 uids = append(uids, v.UID) 376 } 377 } 378 379 usernames, err := h.contactsProvider.FindUsernames(mctx, uids) 380 if err != nil { 381 if requireUsernames { 382 return ret, err 383 } 384 mctx.Warning("Cannot find usernames for search results: %s", err) 385 } 386 387 var serviceMaps map[keybase1.UID]libkb.UserServiceSummary 388 if includeServicesSummary { 389 serviceMaps, err = h.contactsProvider.FindServiceMaps(mctx, uids) 390 if err != nil { 391 mctx.Warning("Cannot get service maps for search results: %s", err) 392 } 393 } 394 395 copyResult := func(slice []imptofuQueryResult, index int, result contacts.ContactLookupResult) { 396 if result.Error != "" || result.UID.IsNil() { 397 return 398 } 399 400 row := slice[index] 401 row.found = true 402 row.UID = result.UID 403 usernamePkg, ok := usernames[result.UID] 404 if ok { 405 row.username = usernamePkg.Username 406 row.fullName = usernamePkg.Fullname 407 } 408 row.serviceMap = serviceMaps[result.UID] 409 410 assertion := row.assertion 411 if result.Coerced != "" && result.Coerced != assertion.GetValue() { 412 // Server corrected our assertion - take it instead of what we have. 413 assertion, err := imptofuQueryToAssertion(mctx.Ctx(), assertion.GetKey(), result.Coerced) 414 if err == nil { 415 row.assertion = assertion 416 } else { 417 mctx.Warning("Failed to create assertion from coerced result: %s", err) 418 } 419 } 420 421 slice[index] = row 422 } 423 424 for i, email := range emails { 425 lookupKey := contacts.MakeEmailLookupKey(email) 426 result, found := lookupRes.Results[lookupKey] 427 if found { 428 copyResult(emailRes, i, result) 429 } 430 } 431 432 for i, phone := range phoneNumbers { 433 lookupKey := contacts.MakePhoneLookupKey(phone) 434 result, found := lookupRes.Results[lookupKey] 435 if found { 436 copyResult(phoneRes, i, result) 437 } 438 } 439 440 return ret, nil 441 } 442 443 func (h *UserSearchHandler) imptofuSearch(mctx libkb.MetaContext, arg keybase1.UserSearchArg) (res []keybase1.APIUserSearchResult, err error) { 444 var emails []keybase1.EmailAddress 445 var phones []keybase1.RawPhoneNumber 446 447 switch arg.Service { 448 case "email": 449 emails = append(emails, keybase1.EmailAddress(arg.Query)) 450 case "phone": 451 phones = append(phones, keybase1.RawPhoneNumber(arg.Query)) 452 default: 453 return nil, fmt.Errorf("unexpected service=%q in imptofuSearch", arg.Service) 454 } 455 456 searchRet, err := h.searchEmailsOrPhoneNumbers(mctx, emails, phones, false /* requireUsernames */, arg.IncludeServicesSummary) 457 if err != nil { 458 return nil, err 459 } 460 461 slice := searchRet.emails 462 slice = append(slice, searchRet.phoneNumbers...) 463 if len(slice) != 1 { 464 return nil, fmt.Errorf("Expected 1 result from `searchEmailsOrPhoneNumbers` but got %d", len(slice)) 465 } 466 467 result := slice[0] 468 if !result.validInput { 469 return nil, fmt.Errorf("Invalid input: %q", result.input) 470 } 471 472 imptofu := &keybase1.ImpTofuSearchResult{ 473 Assertion: result.assertion.String(), 474 AssertionKey: result.assertion.GetKey(), 475 AssertionValue: result.assertion.GetValue(), 476 } 477 var servicesSummary map[keybase1.APIUserServiceID]keybase1.APIUserServiceSummary 478 if result.found { 479 imptofu.KeybaseUsername = result.username 480 imptofu.PrettyName = result.fullName 481 if result.serviceMap != nil { 482 servicesSummary = make(map[keybase1.APIUserServiceID]keybase1.APIUserServiceSummary, len(result.serviceMap)) 483 for serviceID, username := range result.serviceMap { 484 serviceName := keybase1.APIUserServiceID(serviceID) 485 servicesSummary[serviceName] = keybase1.APIUserServiceSummary{ 486 ServiceName: serviceName, 487 Username: username, 488 } 489 } 490 } 491 } 492 493 res = []keybase1.APIUserSearchResult{{ 494 Score: 1.0, 495 Imptofu: imptofu, 496 ServicesSummary: servicesSummary, 497 }} 498 499 return res, nil 500 } 501 502 func (h *UserSearchHandler) makeSearchRequest(mctx libkb.MetaContext, arg keybase1.UserSearchArg) (res []keybase1.APIUserSearchResult, err error) { 503 res, err = h.searchProvider.MakeSearchRequest(mctx, arg) 504 if err != nil { 505 return nil, err 506 } 507 508 // Downcase usernames, pluck raw score into outer struct. 509 for i, row := range res { 510 if row.Keybase != nil { 511 res[i].Keybase.Username = strings.ToLower(row.Keybase.Username) 512 res[i].RawScore = row.Keybase.RawScore 513 } 514 if row.Service != nil { 515 res[i].Service.Username = strings.ToLower(row.Service.Username) 516 } 517 } 518 519 return res, nil 520 } 521 522 func (h *UserSearchHandler) keybaseSearchWithContacts(mctx libkb.MetaContext, arg keybase1.UserSearchArg) (res []keybase1.APIUserSearchResult, err error) { 523 res, err = h.makeSearchRequest(mctx, arg) 524 if err != nil { 525 mctx.Warning("Failed to do an API search for %q: %s", arg.Service, err) 526 } 527 528 if arg.IncludeContacts { 529 contactsRes, err := contactSearch(mctx, arg) 530 if err != nil { 531 mctx.Warning("Failed to do contacts search: %s", err) 532 return res, nil 533 } 534 535 // Filter contacts - If we have a username match coming from the 536 // service, prefer it instead of contact result for the same user 537 // but with SBS assertion in it. 538 usernameSet := make(map[string]struct{}, len(res)) // set of usernames 539 for _, result := range res { 540 if result.Keybase != nil { 541 // All current results should be Keybase but be safe in 542 // case code in this function changes. 543 usernameSet[result.Keybase.Username] = struct{}{} 544 } 545 } 546 547 for _, contact := range contactsRes { 548 if contact.Contact.Resolved { 549 // Do not add this contact result if there already is a 550 // keybase result with username that the contact resolved 551 // to. 552 username := contact.Contact.Username 553 if _, found := usernameSet[username]; found { 554 continue 555 } 556 usernameSet[username] = struct{}{} 557 } 558 res = append(res, contact) 559 } 560 561 sort.Slice(res, func(i, j int) bool { 562 return compareUserSearch(res[i], res[j]) 563 }) 564 565 for i := range res { 566 res[i].Score = 1.0 / float64(1+i) 567 } 568 569 // Trim the whole result to MaxResult. 570 maxRes := arg.MaxResults 571 if maxRes > 0 && len(res) > maxRes { 572 res = res[:maxRes] 573 } 574 } 575 576 return res, nil 577 } 578 579 func (h *UserSearchHandler) UserSearch(ctx context.Context, arg keybase1.UserSearchArg) (res []keybase1.APIUserSearchResult, err error) { 580 mctx := libkb.NewMetaContext(ctx, h.G()).WithLogTag("USEARCH") 581 defer mctx.Trace(fmt.Sprintf("UserSearch#UserSearch(s=%q, q=%q)", arg.Service, arg.Query), 582 &err)() 583 584 if arg.Service == "" { 585 return nil, fmt.Errorf("unexpected empty `Service` argument") 586 } else if arg.IncludeContacts && arg.Service != "keybase" { 587 return nil, fmt.Errorf("`IncludeContacts` is only valid with service=\"keybase\" (got service=%q)", arg.Service) 588 } 589 590 if arg.Query == "" { 591 return nil, nil 592 } 593 594 switch arg.Service { 595 case "keybase": 596 return h.keybaseSearchWithContacts(mctx, arg) 597 case "phone", "email": 598 return h.imptofuSearch(mctx, arg) 599 default: 600 return h.makeSearchRequest(mctx, arg) 601 } 602 } 603 604 func (h *UserSearchHandler) GetNonUserDetails(ctx context.Context, arg keybase1.GetNonUserDetailsArg) (res keybase1.NonUserDetails, err error) { 605 mctx := libkb.NewMetaContext(ctx, h.G()) 606 defer mctx.Trace(fmt.Sprintf("UserSearch#GetNonUserDetails(%q)", arg.Assertion), 607 &err)() 608 609 actx := mctx.G().MakeAssertionContext(mctx) 610 url, err := libkb.ParseAssertionURL(actx, arg.Assertion, true /* strict */) 611 if err != nil { 612 return res, err 613 } 614 615 username := url.GetValue() 616 service := url.GetKey() 617 res.AssertionValue = username 618 res.AssertionKey = service 619 620 if url.IsKeybase() { 621 res.IsNonUser = false 622 res.Description = "Keybase user" 623 return res, nil 624 } 625 626 res.IsNonUser = true 627 assertion := url.String() 628 629 if url.IsSocial() { 630 caser := cases.Title(language.AmericanEnglish) 631 res.Description = fmt.Sprintf("%s user", caser.String(service)) 632 apiRes, err := h.makeSearchRequest(mctx, keybase1.UserSearchArg{ 633 Query: username, 634 Service: service, 635 IncludeServicesSummary: false, 636 MaxResults: 1, 637 }) 638 if err == nil { 639 for _, v := range apiRes { 640 s := v.Service 641 if s != nil && strings.EqualFold(s.Username, username) && string(s.ServiceName) == service { 642 res.Service = s 643 } 644 } 645 } else { 646 mctx.Warning("Can't get external profile data with: %s", err) 647 } 648 649 res.SiteIcon = libkb.MakeProofIcons(mctx, service, libkb.ProofIconTypeSmall, 16) 650 res.SiteIconDarkmode = libkb.MakeProofIcons(mctx, service, libkb.ProofIconTypeSmallDarkmode, 16) 651 res.SiteIconFull = libkb.MakeProofIcons(mctx, service, libkb.ProofIconTypeFull, 64) 652 res.SiteIconFullDarkmode = libkb.MakeProofIcons(mctx, service, libkb.ProofIconTypeFullDarkmode, 64) 653 } else if service == "phone" || service == "email" { 654 contacts, err := mctx.G().SyncedContactList.RetrieveContacts(mctx) 655 if err == nil { 656 for _, v := range contacts { 657 if v.Assertion == assertion { 658 contact := v 659 res.Contact = &contact 660 break 661 } 662 } 663 } else { 664 mctx.Warning("Can't get contact list to match assertion: %s", err) 665 } 666 667 switch service { 668 case "phone": 669 res.Description = "Phone contact" 670 case "email": 671 res.Description = "E-mail contact" 672 } 673 } 674 675 return res, nil 676 } 677 678 func (h *UserSearchHandler) BulkEmailOrPhoneSearch(ctx context.Context, 679 arg keybase1.BulkEmailOrPhoneSearchArg) (ret []keybase1.EmailOrPhoneNumberSearchResult, err error) { 680 681 mctx := libkb.NewMetaContext(ctx, h.G()) 682 defer mctx.Trace(fmt.Sprintf("UserSearch#BulkEmailOrPhoneSearch(%d emails,%d phones)", 683 len(arg.Emails), len(arg.PhoneNumbers)), &err)() 684 685 // Use `emails` package to split comma/newline separated list of emails 686 // into actual list of valid emails. 687 emailStrings := email_utils.ParseSeparatedEmails(mctx, arg.Emails, nil /* malformed */) 688 emails := make([]keybase1.EmailAddress, len(emailStrings)) 689 for i, v := range emailStrings { 690 emails[i] = keybase1.EmailAddress(v) 691 } 692 693 // We ask callers to give us valid phone numbers as the argument even 694 // though `searchEmailsOrPhoneNumbers` could handle invalid or 695 // mis-formatted numbers as well (in theory). 696 697 // TODO: It's probably a good idea to figure out which one it is and clean 698 // this code up. 699 700 phones := make([]keybase1.RawPhoneNumber, len(arg.PhoneNumbers)) 701 for i, v := range arg.PhoneNumbers { 702 phones[i] = keybase1.RawPhoneNumber(v) 703 } 704 705 searchRet, err := h.searchEmailsOrPhoneNumbers(mctx, emails, phones, 706 false /* requireUsernames */, false /* includeServiceSummary */) 707 if err != nil { 708 return ret, err 709 } 710 711 // Caller shouldn't care about the ordering here, we are mixing everything 712 // together and returning as one list. 713 all := searchRet.emails 714 all = append(all, searchRet.phoneNumbers...) 715 ret = make([]keybase1.EmailOrPhoneNumberSearchResult, 0, len(all)) 716 for _, result := range all { 717 if !result.validInput { 718 continue 719 } 720 721 // Localize result to keybase1 type. 722 locRes := keybase1.EmailOrPhoneNumberSearchResult{ 723 Input: result.input, 724 Assertion: result.assertion.String(), 725 AssertionKey: result.assertion.GetKey(), 726 AssertionValue: result.assertion.GetValue(), 727 } 728 if result.found && result.username != "" { 729 locRes.FoundUser = result.found 730 locRes.Username = result.username 731 locRes.FullName = result.fullName 732 } 733 734 ret = append(ret, locRes) 735 } 736 737 return ret, nil 738 }