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  }