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