1// Copyright 2015 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package engine
5
6import (
7	"errors"
8	"fmt"
9	"regexp"
10	"sort"
11	"time"
12
13	"github.com/keybase/client/go/libkb"
14	keybase1 "github.com/keybase/client/go/protocol/keybase1"
15	jsonw "github.com/keybase/go-jsonw"
16)
17
18type ListTrackingEngineArg struct {
19	Assertion  string
20	UID        keybase1.UID
21	CachedOnly bool
22
23	// If CachedOnly is set and StalenessWindow is non-nil, will load with
24	// StaleOK and use the relaxed CachedOnlyStalenessWindow instead.
25	CachedOnlyStalenessWindow *time.Duration
26
27	JSON    bool
28	Verbose bool
29	Filter  string
30}
31
32// ListTrackingEngine loads the follows of the given user using their sigchain,
33// but relies on the server to filter out users who have reset after the follow
34// statement.
35type ListTrackingEngine struct {
36	arg         *ListTrackingEngineArg
37	tableResult keybase1.UserSummarySet
38	jsonResult  string
39	libkb.Contextified
40
41	disableTrackerSyncerForTest bool
42}
43
44func NewListTrackingEngine(g *libkb.GlobalContext, arg *ListTrackingEngineArg) *ListTrackingEngine {
45	return &ListTrackingEngine{
46		arg:          arg,
47		Contextified: libkb.NewContextified(g),
48	}
49}
50
51func (e *ListTrackingEngine) Name() string {
52	return "ListTracking"
53}
54
55func (e *ListTrackingEngine) Prereqs() Prereqs { return Prereqs{} }
56
57func (e *ListTrackingEngine) RequiredUIs() []libkb.UIKind { return []libkb.UIKind{} }
58
59func (e *ListTrackingEngine) SubConsumers() []libkb.UIConsumer { return nil }
60
61func (e *ListTrackingEngine) Run(m libkb.MetaContext) (err error) {
62	uid, err := lookupUID(m, e.arg.UID, e.arg.Assertion, e.arg.CachedOnly)
63	if err != nil {
64		return err
65	}
66
67	// Get version according to server so we can filter out reset users later
68	ts := libkb.NewServertrustTrackerSyncer(m.G(), m.G().GetMyUID(), libkb.FollowDirectionFollowing)
69	var tsErr error
70	if e.disableTrackerSyncerForTest {
71		tsErr = errors.New("tracker syncer disabled for test")
72	} else if e.arg.CachedOnly {
73		tsErr = libkb.RunSyncerCached(m, ts, uid)
74	} else {
75		tsErr = libkb.RunSyncer(m, ts, uid, false /* loggedIn */, false /* forceReload */)
76	}
77	useServerLookup := false
78	serverLookup := make(map[keybase1.UID]struct{})
79	fullNames := make(map[keybase1.UID]string)
80	if tsErr != nil {
81		m.Warning("failed to load following list from server (cachedOnly=%t); continuing: %s", e.arg.CachedOnly, tsErr)
82	} else {
83		useServerLookup = true
84		m.Debug("got following list from server (len=%d, cachedOnly=%t); using it to filter sigchain list", len(ts.Result().Users), e.arg.CachedOnly)
85		for _, user := range ts.Result().Users {
86			serverLookup[user.Uid] = struct{}{}
87			fullNames[user.Uid] = user.FullName
88		}
89	}
90
91	// Load unstubbed so we get track links
92	larg := libkb.NewLoadUserArgWithMetaContext(m).
93		WithUID(uid).
94		WithStubMode(libkb.StubModeUnstubbed).
95		WithCachedOnly(e.arg.CachedOnly).
96		WithSelf(uid.Exists() && uid.Equal(m.G().GetMyUID()))
97	if e.arg.CachedOnly && e.arg.CachedOnlyStalenessWindow != nil {
98		larg = larg.WithStaleOK(true)
99	}
100	upak, _, err := m.G().GetUPAKLoader().LoadV2(larg)
101	if err != nil {
102		return err
103	}
104	if upak == nil {
105		return libkb.UserNotFoundError{}
106	}
107
108	if e.arg.CachedOnly && e.arg.CachedOnlyStalenessWindow != nil {
109		if m.G().Clock().Since(keybase1.FromTime(upak.Uvv.CachedAt)) > *e.arg.CachedOnlyStalenessWindow {
110			msg := fmt.Sprintf("upak was cached but exceeded custom staleness window %v", *e.arg.CachedOnlyStalenessWindow)
111			return libkb.UserNotFoundError{UID: uid, Msg: msg}
112		}
113	}
114
115	unfilteredTracks := upak.Current.RemoteTracks
116
117	var rxx *regexp.Regexp
118	if e.arg.Filter != "" {
119		rxx, err = regexp.Compile(e.arg.Filter)
120		if err != nil {
121			return err
122		}
123	}
124
125	// Filter out any marked reset by server, or due to Filter argument
126	var filteredTracks []keybase1.RemoteTrack
127	for _, track := range unfilteredTracks {
128		trackedUID := track.Uid
129		if useServerLookup {
130			if _, ok := serverLookup[trackedUID]; !ok {
131				m.Debug("filtering out uid %s in sigchain list but not provided by server", trackedUID)
132				continue
133			}
134		}
135		if rxx != nil && !rxx.MatchString(track.Username) {
136			continue
137		}
138		filteredTracks = append(filteredTracks, track)
139	}
140
141	sort.Slice(filteredTracks, func(i, j int) bool {
142		return filteredTracks[i].Username < filteredTracks[j].Username
143	})
144
145	if e.arg.JSON {
146		return e.runJSON(m, filteredTracks, e.arg.Verbose)
147	}
148	return e.runTable(m, filteredTracks, fullNames)
149}
150
151func (e *ListTrackingEngine) runTable(m libkb.MetaContext, filteredTracks []keybase1.RemoteTrack, fullNames map[keybase1.UID]string) error {
152	e.tableResult = keybase1.UserSummarySet{}
153	for _, track := range filteredTracks {
154		linkID := track.LinkID
155		entry := keybase1.UserSummary{
156			Uid:      track.Uid,
157			Username: track.Username,
158			LinkID:   &linkID,
159			FullName: fullNames[track.Uid],
160		}
161		e.tableResult.Users = append(e.tableResult.Users, entry)
162	}
163	return nil
164}
165
166func condenseRecord(t keybase1.RemoteTrack) (*jsonw.Wrapper, error) {
167	out := jsonw.NewDictionary()
168	err := out.SetKey("uid", libkb.UIDWrapper(t.Uid))
169	if err != nil {
170		return nil, err
171	}
172	err = out.SetKey("username", jsonw.NewString(t.Username))
173	if err != nil {
174		return nil, err
175	}
176	err = out.SetKey("link_id", jsonw.NewString(t.LinkID.String()))
177	if err != nil {
178		return nil, err
179	}
180
181	return out, nil
182}
183
184func (e *ListTrackingEngine) runJSON(m libkb.MetaContext, filteredTracks []keybase1.RemoteTrack, verbose bool) error {
185	var tmp []*jsonw.Wrapper
186	for _, track := range filteredTracks {
187		var rec *jsonw.Wrapper
188		var e2 error
189		if rec, e2 = condenseRecord(track); e2 != nil {
190			m.Warning("In conversion to JSON: %s", e2)
191		}
192		if e2 == nil {
193			tmp = append(tmp, rec)
194		}
195	}
196
197	ret := jsonw.NewArray(len(tmp))
198	for i, r := range tmp {
199		if err := ret.SetIndex(i, r); err != nil {
200			return err
201		}
202	}
203
204	e.jsonResult = ret.MarshalPretty()
205	return nil
206}
207
208func (e *ListTrackingEngine) TableResult() keybase1.UserSummarySet {
209	return e.tableResult
210}
211
212func (e *ListTrackingEngine) JSONResult() string {
213	return e.jsonResult
214}
215