1// Copyright 2015 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package libkb
5
6import (
7	"fmt"
8	"sync"
9	"time"
10
11	keybase1 "github.com/keybase/client/go/protocol/keybase1"
12)
13
14type FollowDirection int
15
16const (
17	FollowDirectionFollowing FollowDirection = 0
18	FollowDirectionFollowers FollowDirection = 1
19)
20
21func directionToReverse(direction FollowDirection) (reverse bool) {
22	return direction == FollowDirectionFollowing
23}
24
25type ServertrustTrackerSyncer struct {
26	sync.Mutex
27	Contextified
28	res       *keybase1.UserSummarySet
29	direction FollowDirection
30	dirty     bool
31	callerUID keybase1.UID
32}
33
34const cacheTimeout = 10 * time.Minute
35
36func (t *ServertrustTrackerSyncer) dbKey(u keybase1.UID) DbKey {
37	if t.direction == FollowDirectionFollowing {
38		return DbKeyUID(DBUnverifiedTrackersFollowing, u)
39	}
40	return DbKeyUID(DBUnverifiedTrackersFollowers, u)
41}
42
43func (t *ServertrustTrackerSyncer) loadFromStorage(m MetaContext, uid keybase1.UID, useExpiration bool) error {
44	var err error
45	var found bool
46	var tmp keybase1.UserSummarySet
47	defer m.Trace(fmt.Sprintf("loadFromStorage(%s)", uid), &err)()
48	found, err = t.G().LocalDb.GetInto(&tmp, t.dbKey(uid))
49	if err != nil {
50		return err
51	}
52	if !found {
53		m.Debug("| no cached copy found")
54		return nil
55	}
56	cachedAt := keybase1.FromTime(tmp.Time)
57	if useExpiration && time.Since(cachedAt) > cacheTimeout {
58		m.Debug("| expired; cached at %s", cachedAt)
59		return nil
60	}
61	m.Debug("| found a record, cached %s", cachedAt)
62	t.res = &tmp
63	return nil
64}
65
66func (t *ServertrustTrackerSyncer) getLoadedVersion() int {
67	ret := -1
68	if t.res != nil {
69		ret = t.res.Version
70	}
71	return ret
72}
73
74func (t *ServertrustTrackerSyncer) syncFromServer(m MetaContext, uid keybase1.UID, forceReload bool) (err error) {
75
76	defer m.Trace(fmt.Sprintf("syncFromServer(%s)", uid), &err)()
77
78	hargs := HTTPArgs{
79		"uid":        UIDArg(uid),
80		"reverse":    B{directionToReverse(t.direction)},
81		"autoCamel":  B{true},
82		"caller_uid": UIDArg(t.callerUID),
83	}
84	lv := t.getLoadedVersion()
85	if lv >= 0 && !forceReload {
86		hargs.Add("version", I{lv})
87	}
88	var res *APIRes
89	res, err = m.G().API.Get(m, APIArg{
90		Endpoint: "user/list_followers_for_display",
91		Args:     hargs,
92	})
93	m.Debug("| syncFromServer() -> %s", ErrToOk(err))
94	if err != nil {
95		return err
96	}
97	var tmp keybase1.UserSummarySet
98	if err = res.Body.UnmarshalAgain(&tmp); err != nil {
99		return
100	}
101	tmp.Time = keybase1.ToTime(time.Now())
102	if lv < 0 || tmp.Version > lv || forceReload {
103		m.Debug("| syncFromServer(): got update %d > %d (%d records)", tmp.Version, lv,
104			len(tmp.Users))
105		t.res = &tmp
106		t.dirty = true
107	} else {
108		m.Debug("| syncFromServer(): no change needed @ %d", lv)
109	}
110	return nil
111}
112
113func (t *ServertrustTrackerSyncer) store(m MetaContext, uid keybase1.UID) error {
114	var err error
115	if !t.dirty {
116		return err
117	}
118
119	if err = t.G().LocalDb.PutObj(t.dbKey(uid), nil, t.res); err != nil {
120		return err
121	}
122
123	t.dirty = false
124	return nil
125}
126
127func (t *ServertrustTrackerSyncer) needsLogin(m MetaContext) bool {
128	return false
129}
130
131func (t *ServertrustTrackerSyncer) Block(m MetaContext, badUIDs map[keybase1.UID]bool) (err error) {
132	defer m.Trace(fmt.Sprintf("ServertrustTrackerSyncer#Block(%+v)", badUIDs), &err)()
133	t.Lock()
134	defer t.Unlock()
135
136	if t.direction != FollowDirectionFollowers {
137		return fmt.Errorf("can only delete users out of followers cache")
138	}
139
140	if t.res == nil {
141		m.Debug("No followers loaded, so nothing to do")
142		return nil
143	}
144
145	err = t.loadFromStorage(m, t.callerUID, true)
146	if err != nil {
147		return err
148	}
149
150	var newUsers []keybase1.UserSummary
151	for _, userSummary := range t.res.Users {
152		if badUIDs[userSummary.Uid] {
153			m.Debug("Filtering bad user out of state: %s", userSummary.Uid)
154			t.dirty = true
155		} else {
156			newUsers = append(newUsers, userSummary)
157		}
158	}
159	t.res.Users = newUsers
160	err = t.store(m, t.callerUID)
161	return err
162}
163
164func (t *ServertrustTrackerSyncer) Result() keybase1.UserSummarySet {
165	if t.res == nil {
166		return keybase1.UserSummarySet{}
167	}
168
169	// Normalize usernames
170	var normalizedUsers []keybase1.UserSummary
171	for _, u := range t.res.Users {
172		normalizedUser := u
173		normalizedUser.Username = NewNormalizedUsername(u.Username).String()
174		normalizedUsers = append(normalizedUsers, normalizedUser)
175	}
176	t.res.Users = normalizedUsers
177
178	return *t.res
179}
180
181func NewServertrustTrackerSyncer(g *GlobalContext, callerUID keybase1.UID, direction FollowDirection) *ServertrustTrackerSyncer {
182	return &ServertrustTrackerSyncer{
183		Contextified: NewContextified(g),
184		direction:    direction,
185		callerUID:    callerUID,
186	}
187}
188