github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  }