1package contacts
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/keybase/client/go/encrypteddb"
8
9	"github.com/keybase/client/go/libkb"
10	"github.com/keybase/client/go/protocol/keybase1"
11)
12
13// Saving contact list into encrypted db.
14
15// Cache resolutions of a lookup ran on entire contact list provided by the
16// frontend. Assume every time SaveContacts is called, entire contact list is
17// passed as an argument. Always cache the result of last resolution, do not do
18// any result merging.
19
20type SavedContactsStore struct {
21	encryptedDB *encrypteddb.EncryptedDB
22}
23
24var _ libkb.SyncedContactListProvider = (*SavedContactsStore)(nil)
25
26// NewSavedContactsStore creates a new SavedContactsStore for global context.
27// The store is used to securely store list of resolved contacts.
28func NewSavedContactsStore(g *libkb.GlobalContext) *SavedContactsStore {
29	keyFn := func(ctx context.Context) ([32]byte, error) {
30		return encrypteddb.GetSecretBoxKey(ctx, g,
31			libkb.EncryptionReasonContactsLocalStorage, "encrypting local contact list")
32	}
33	dbFn := func(g *libkb.GlobalContext) *libkb.JSONLocalDb {
34		return g.LocalDb
35	}
36	return &SavedContactsStore{
37		encryptedDB: encrypteddb.New(g, dbFn, keyFn),
38	}
39}
40
41func ServiceInit(g *libkb.GlobalContext) {
42	g.SyncedContactList = NewSavedContactsStore(g)
43}
44
45func savedContactsDbKey(uid keybase1.UID) libkb.DbKey {
46	return libkb.DbKey{
47		Typ: libkb.DBSavedContacts,
48		Key: fmt.Sprintf("%v", uid),
49	}
50}
51
52type savedContactsCache struct {
53	Contacts []keybase1.ProcessedContact
54	Version  int
55}
56
57const savedContactsCurrentVer = 1
58
59func assertionToNameDbKey(uid keybase1.UID) libkb.DbKey {
60	return libkb.DbKey{
61		Typ: libkb.DBSavedContacts,
62		Key: fmt.Sprintf("lookup:%v", uid),
63	}
64}
65
66type assertionToNameCache struct {
67	AssertionToName map[string]string
68	Version         int
69}
70
71const assertionToNameCurrentVer = 1
72
73func ResolveAndSaveContacts(mctx libkb.MetaContext, provider ContactsProvider, contacts []keybase1.Contact) (res keybase1.ContactListResolutionResult, err error) {
74	resolveResults, err := ResolveContacts(mctx, provider, contacts)
75	if err != nil {
76		return res, err
77	}
78
79	// find resolved contacts
80	for _, contact := range resolveResults {
81		// Strip out the user and anyone they follow.
82		if contact.Resolved && !contact.Following &&
83			libkb.NewNormalizedUsername(contact.Username) != mctx.CurrentUsername() {
84			res.Resolved = append(res.Resolved, contact)
85		}
86	}
87
88	// find newly resolved
89	s := mctx.G().SyncedContactList
90	currentContacts, err := s.RetrieveContacts(mctx)
91
92	newlyResolvedMap := make(map[string]keybase1.ProcessedContact)
93	if err == nil {
94		unresolved := make(map[string]bool)
95		resolved := make(map[string]bool)
96		for _, contact := range currentContacts {
97			if contact.Resolved {
98				resolved[contact.ContactName] = true
99			}
100			if resolved[contact.ContactName] {
101				// If any contact by this name is resolved, we dedupe.
102				delete(unresolved, contact.ContactName)
103			} else {
104				// We resolve based on display name, not assertion, so we don't
105				// duplicate multiple assertions for the same contact.
106				unresolved[contact.ContactName] = true
107			}
108		}
109
110		for _, resolution := range resolveResults {
111			if unresolved[resolution.ContactName] && resolution.Resolved && !resolution.Following {
112				// We only want to show one resolution per username.
113				newlyResolvedMap[resolution.Username] = resolution
114			}
115		}
116	} else {
117		mctx.Warning("error retrieving synced contacts; continuing: %s", err)
118	}
119
120	if len(newlyResolvedMap) == 0 {
121		return res, s.SaveProcessedContacts(mctx, resolveResults)
122	}
123
124	resolutionsForPeoplePage := make([]ContactResolution, 0, len(newlyResolvedMap))
125	for _, contact := range newlyResolvedMap {
126		contactDisplay := contact.ContactName
127		if contactDisplay == "" {
128			contactDisplay = contact.Component.ValueString()
129		}
130		resolutionsForPeoplePage = append(resolutionsForPeoplePage, ContactResolution{
131			Description: contactDisplay,
132			ResolvedUser: keybase1.User{
133				Uid:      contact.Uid,
134				Username: contact.Username,
135			},
136		})
137		res.NewlyResolved = append(res.NewlyResolved, contact)
138	}
139	err = SendEncryptedContactResolutionToServer(mctx, resolutionsForPeoplePage)
140	if err != nil {
141		mctx.Warning("Could not add resolved contacts to people page: %v; returning contacts anyway", err)
142	}
143
144	return res, s.SaveProcessedContacts(mctx, resolveResults)
145}
146
147func makeAssertionToName(contacts []keybase1.ProcessedContact) (res map[string]string) {
148	res = make(map[string]string)
149	toRemove := make(map[string]struct{})
150	for _, contact := range contacts {
151		if _, ok := res[contact.Assertion]; ok {
152			// multiple contacts match this assertion, remove once we're done
153			toRemove[contact.Assertion] = struct{}{}
154			continue
155		}
156		res[contact.Assertion] = contact.ContactName
157	}
158	for remove := range toRemove {
159		delete(res, remove)
160	}
161	return res
162}
163
164func (s *SavedContactsStore) SaveProcessedContacts(mctx libkb.MetaContext, contacts []keybase1.ProcessedContact) (err error) {
165	val := savedContactsCache{
166		Contacts: contacts,
167		Version:  savedContactsCurrentVer,
168	}
169
170	cacheKey := savedContactsDbKey(mctx.CurrentUID())
171	err = s.encryptedDB.Put(mctx.Ctx(), cacheKey, val)
172	if err != nil {
173		return err
174	}
175
176	assertionToName := makeAssertionToName(contacts)
177	lookupVal := assertionToNameCache{
178		AssertionToName: assertionToName,
179		Version:         assertionToNameCurrentVer,
180	}
181
182	cacheKey = assertionToNameDbKey(mctx.CurrentUID())
183	err = s.encryptedDB.Put(mctx.Ctx(), cacheKey, lookupVal)
184	return err
185}
186
187func (s *SavedContactsStore) RetrieveContacts(mctx libkb.MetaContext) (ret []keybase1.ProcessedContact, err error) {
188	cacheKey := savedContactsDbKey(mctx.CurrentUID())
189	var cache savedContactsCache
190	found, err := s.encryptedDB.Get(mctx.Ctx(), cacheKey, &cache)
191	if err != nil {
192		return nil, err
193	}
194	if !found {
195		return ret, nil
196	}
197	if cache.Version != savedContactsCurrentVer {
198		mctx.Warning("synced contact list found but had an old version (found: %d, need: %d), returning empty list",
199			cache.Version, savedContactsCurrentVer)
200		return ret, nil
201	}
202	return cache.Contacts, nil
203}
204
205func (s *SavedContactsStore) RetrieveAssertionToName(mctx libkb.MetaContext) (ret map[string]string, err error) {
206	cacheKey := assertionToNameDbKey(mctx.CurrentUID())
207	var cache assertionToNameCache
208	found, err := s.encryptedDB.Get(mctx.Ctx(), cacheKey, &cache)
209	if err != nil {
210		return nil, err
211	}
212	if !found {
213		return ret, nil
214	}
215	if cache.Version != assertionToNameCurrentVer {
216		mctx.Warning("assertion to name found but had an old version (found: %d, need: %d), returning empty map",
217			cache.Version, assertionToNameCurrentVer)
218		return ret, nil
219	}
220	return cache.AssertionToName, nil
221}
222
223func (s *SavedContactsStore) UnresolveContactsWithComponent(mctx libkb.MetaContext,
224	phoneNumber *keybase1.PhoneNumber, email *keybase1.EmailAddress) {
225	// TODO: Use a phoneNumber | email variant instead of two pointers.
226	contactList, err := s.RetrieveContacts(mctx)
227	if err != nil {
228		mctx.Warning("Failed to get cached contact list: %x", err)
229		return
230	}
231	for i, con := range contactList {
232		var unresolve bool
233		switch {
234		case phoneNumber != nil && con.Component.PhoneNumber != nil:
235			unresolve = *con.Component.PhoneNumber == keybase1.RawPhoneNumber(*phoneNumber)
236		case email != nil && con.Component.Email != nil:
237			unresolve = *con.Component.Email == *email
238		}
239
240		if unresolve {
241			// Unresolve contact.
242			con.Resolved = false
243			con.Username = ""
244			con.Uid = ""
245			con.Following = false
246			con.FullName = ""
247			// TODO: DisplayName/DisplayLabel logic infects yet another file /
248			// module. But it will sort itself out once we get rid of both.
249			con.DisplayName = con.ContactName
250			con.DisplayLabel = con.Component.FormatDisplayLabel(false /* addLabel */)
251			contactList[i] = con
252		}
253	}
254	err = s.SaveProcessedContacts(mctx, contactList)
255	if err != nil {
256		mctx.Warning("Failed to put cached contact list: %x", err)
257	}
258}
259