github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/teams/opensearch/search.go (about) 1 // Copyright 2020 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package opensearch 5 6 import ( 7 "errors" 8 "sort" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/keybase/client/go/libkb" 14 "github.com/keybase/client/go/protocol/gregor1" 15 "github.com/keybase/client/go/protocol/keybase1" 16 ) 17 18 type teamMap map[keybase1.TeamID]keybase1.TeamSearchItem 19 20 const refreshThreshold = time.Hour 21 22 var lastRefresh time.Time 23 24 type teamSearchResult struct { 25 Results []keybase1.TeamSearchItem `json:"results"` 26 Status libkb.AppStatus 27 } 28 29 func (r *teamSearchResult) GetAppStatus() *libkb.AppStatus { 30 return &r.Status 31 } 32 33 type teamRefreshResult struct { 34 keybase1.TeamSearchExport 35 Status libkb.AppStatus 36 } 37 38 func (r *teamRefreshResult) GetAppStatus() *libkb.AppStatus { 39 return &r.Status 40 } 41 42 type storageItem struct { 43 Items teamMap 44 Suggested []keybase1.TeamID 45 Hash string 46 } 47 48 func dbKey() libkb.DbKey { 49 return libkb.DbKey{ 50 Typ: libkb.DBOpenTeams, 51 Key: "v0", 52 } 53 } 54 55 func getCurrentHash(mctx libkb.MetaContext) (hash string) { 56 var si storageItem 57 found, err := mctx.G().GetKVStore().GetInto(&si, dbKey()) 58 if err != nil { 59 mctx.Debug("OpenSearch.getCurrentHash: failed to read: %s", err) 60 return "" 61 } 62 if !found { 63 return "" 64 } 65 return si.Hash 66 } 67 68 func getOpenTeams(mctx libkb.MetaContext) (res storageItem, err error) { 69 get := func() (res storageItem, err error) { 70 found, err := mctx.G().GetKVStore().GetInto(&res, dbKey()) 71 if err != nil { 72 return res, err 73 } 74 if !found { 75 return res, errors.New("no open teams found") 76 } 77 return res, nil 78 } 79 if res, err = get(); err != nil { 80 mctx.Debug("OpenSearch.getOpenTeams: failed to get open teams, refreshing") 81 refreshOpenTeams(mctx, true) 82 return get() 83 } 84 return res, nil 85 } 86 87 var refreshMu sync.Mutex 88 89 func refreshOpenTeams(mctx libkb.MetaContext, force bool) { 90 tracer := mctx.G().CTimeTracer(mctx.Ctx(), "OpenSearch.refreshOpenTeams", true) 91 defer tracer.Finish() 92 refreshMu.Lock() 93 defer refreshMu.Unlock() 94 if !force && time.Since(lastRefresh) < refreshThreshold { 95 return 96 } 97 saved := true 98 defer func() { 99 if saved { 100 lastRefresh = time.Now() 101 } 102 }() 103 hash := getCurrentHash(mctx) 104 mctx.Debug("OpenSearch.refreshOpenTeams: using hash: %s", hash) 105 a := libkb.NewAPIArg("teamsearch/refresh") 106 a.Args = libkb.HTTPArgs{} 107 a.Args["hash"] = libkb.S{Val: hash} 108 a.SessionType = libkb.APISessionTypeREQUIRED 109 var apiRes teamRefreshResult 110 if err := mctx.G().API.GetDecode(mctx, a, &apiRes); err != nil { 111 mctx.Debug("OpenSearch.refreshOpenTeams: failed to fetch open teams: %s", err) 112 saved = false 113 return 114 } 115 if len(apiRes.Items) == 0 { 116 mctx.Debug("OpenSearch.refreshOpenTeams: hash match, standing pat") 117 return 118 } 119 mctx.Debug("OpenSearch.refreshOpenTeams: received %d teams, suggested: %d", len(apiRes.Items), 120 len(apiRes.Suggested)) 121 var out storageItem 122 out.Items = apiRes.Items 123 out.Suggested = apiRes.Suggested 124 out.Hash = apiRes.Hash() 125 if err := mctx.G().GetKVStore().PutObj(dbKey(), nil, out); err != nil { 126 mctx.Debug("OpenSearch.refreshOpenTeams: failed to put: %s", err) 127 saved = false 128 return 129 } 130 } 131 132 // Local performs a local search for Keybase open teams. 133 func Local(mctx libkb.MetaContext, query string, limit int) (res []keybase1.TeamSearchItem, err error) { 134 var si storageItem 135 mctx = mctx.WithLogTag("OTS") 136 tracer := mctx.G().CTimeTracer(mctx.Ctx(), "OpenSearch.Local", true) 137 defer tracer.Finish() 138 defer func() { 139 go refreshOpenTeams(mctx, false) 140 }() 141 if si, err = getOpenTeams(mctx); err != nil { 142 return res, err 143 } 144 query = strings.ToLower(query) 145 var results []rankedSearchItem 146 if len(query) == 0 { 147 for index, id := range si.Suggested { 148 results = append(results, rankedSearchItem{ 149 item: si.Items[id], 150 score: 100.0 + float64((len(si.Suggested) - index)), 151 }) 152 } 153 } else { 154 for _, item := range si.Items { 155 rankedItem := rankedSearchItem{ 156 item: item, 157 } 158 rankedItem.score = rankedItem.Score(query) 159 if FilterScore(rankedItem.score) { 160 continue 161 } 162 results = append(results, rankedItem) 163 } 164 } 165 sort.Slice(results, func(i, j int) bool { 166 return results[i].score > results[j].score 167 }) 168 for index, r := range results { 169 if index >= limit { 170 break 171 } 172 if r.item.InTeam, err = mctx.G().ChatHelper.InTeam(mctx.Ctx(), 173 gregor1.UID(mctx.G().GetMyUID().ToBytes()), r.item.Id); err != nil { 174 mctx.Debug("OpenSearch.Local: failed to get inTeam for: %s err: %s", r.item.Id, err) 175 } 176 res = append(res, r.item) 177 } 178 return res, nil 179 } 180 181 func Remote(mctx libkb.MetaContext, query string, limit int) ([]keybase1.TeamSearchItem, error) { 182 tracer := mctx.G().CTimeTracer(mctx.Ctx(), "OpenSearch.Remote", true) 183 defer tracer.Finish() 184 185 a := libkb.NewAPIArg("teamsearch/search") 186 a.Args = libkb.HTTPArgs{} 187 a.Args["query"] = libkb.S{Val: query} 188 a.Args["limit"] = libkb.I{Val: limit} 189 190 a.SessionType = libkb.APISessionTypeREQUIRED 191 var res teamSearchResult 192 if err := mctx.G().API.GetDecode(mctx, a, &res); err != nil { 193 return nil, err 194 } 195 return res.Results, nil 196 }