github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/teambot/featuredbot.go (about)

     1  package teambot
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/keybase/client/go/chat/utils"
    10  	"github.com/keybase/client/go/libkb"
    11  	"github.com/keybase/client/go/protocol/gregor1"
    12  	"github.com/keybase/client/go/protocol/keybase1"
    13  	"github.com/keybase/client/go/teams/opensearch"
    14  )
    15  
    16  const (
    17  	refreshLifetime = 2 * time.Hour
    18  	cacheLifetime   = 24 * time.Hour
    19  )
    20  
    21  type featuredBotsCache struct {
    22  	Data  keybase1.FeaturedBotsRes `codec:"d" json:"d"`
    23  	Ctime gregor1.Time             `codec:"c" json:"c"`
    24  }
    25  
    26  func (c featuredBotsCache) isFresh() bool {
    27  	return time.Since(c.Ctime.Time()) <= cacheLifetime
    28  }
    29  
    30  type FeaturedBotLoader struct {
    31  	libkb.Contextified
    32  }
    33  
    34  func NewFeaturedBotLoader(g *libkb.GlobalContext) *FeaturedBotLoader {
    35  	return &FeaturedBotLoader{
    36  		Contextified: libkb.NewContextified(g),
    37  	}
    38  }
    39  
    40  func (l *FeaturedBotLoader) debug(mctx libkb.MetaContext, msg string, args ...interface{}) {
    41  	l.G().Log.CDebugf(mctx.Ctx(), "FeaturedBotLoader: %s", fmt.Sprintf(msg, args...))
    42  }
    43  
    44  func (l *FeaturedBotLoader) SearchLocal(mctx libkb.MetaContext, arg keybase1.SearchLocalArg) (res keybase1.SearchRes, err error) {
    45  	defer mctx.Trace("FeaturedBotLoader: SearchLocal", &err)()
    46  	if arg.Limit == 0 {
    47  		return res, nil
    48  	}
    49  	bots, err := l.AllFeaturedBots(mctx, arg.SkipCache)
    50  	if err != nil {
    51  		return res, err
    52  	}
    53  
    54  	var results []rankedSearchItem
    55  	if len(arg.Query) == 0 {
    56  		for _, bot := range bots.Bots {
    57  			score := float64(bot.Rank)
    58  			if !bot.IsPromoted || opensearch.FilterScore(score) {
    59  				continue
    60  			}
    61  			results = append(results, rankedSearchItem{
    62  				item:  bot,
    63  				score: score,
    64  			})
    65  		}
    66  	} else {
    67  		query := strings.ToLower(arg.Query)
    68  		for _, item := range bots.Bots {
    69  			rankedItem := rankedSearchItem{
    70  				item: item,
    71  			}
    72  			rankedItem.score = rankedItem.Score(query)
    73  			if opensearch.FilterScore(rankedItem.score) {
    74  				continue
    75  			}
    76  			results = append(results, rankedItem)
    77  		}
    78  	}
    79  	sort.Slice(results, func(i, j int) bool {
    80  		return results[i].score > results[j].score
    81  	})
    82  	res.IsLastPage = true
    83  	for index, r := range results {
    84  		if index >= arg.Limit {
    85  			res.IsLastPage = false
    86  			break
    87  		}
    88  		res.Bots = append(res.Bots, r.item)
    89  	}
    90  	return res, nil
    91  }
    92  
    93  func (l *FeaturedBotLoader) Search(mctx libkb.MetaContext, arg keybase1.SearchArg) (res keybase1.SearchRes, err error) {
    94  	defer mctx.Trace("FeaturedBotLoader: Search", &err)()
    95  	defer func() {
    96  		if err == nil {
    97  			res.Bots = l.present(mctx, res.Bots)
    98  		}
    99  	}()
   100  	apiRes, err := mctx.G().API.Get(mctx, libkb.APIArg{
   101  		Endpoint:    "featured_bots/search",
   102  		SessionType: libkb.APISessionTypeNONE,
   103  		Args: libkb.HTTPArgs{
   104  			"query":  libkb.S{Val: arg.Query},
   105  			"limit":  libkb.I{Val: arg.Limit},
   106  			"offset": libkb.I{Val: arg.Offset},
   107  		},
   108  	})
   109  	if err != nil {
   110  		return res, err
   111  	}
   112  
   113  	err = apiRes.Body.UnmarshalAgain(&res)
   114  	return res, err
   115  }
   116  
   117  func (l *FeaturedBotLoader) featuredBotsFromServer(mctx libkb.MetaContext, arg keybase1.FeaturedBotsArg) (res keybase1.FeaturedBotsRes, err error) {
   118  	apiRes, err := mctx.G().API.Get(mctx, libkb.APIArg{
   119  		Endpoint:    "featured_bots/featured",
   120  		SessionType: libkb.APISessionTypeNONE,
   121  		Args: libkb.HTTPArgs{
   122  			"limit":  libkb.I{Val: arg.Limit},
   123  			"offset": libkb.I{Val: arg.Offset},
   124  		},
   125  	})
   126  	if err != nil {
   127  		return res, err
   128  	}
   129  	err = apiRes.Body.UnmarshalAgain(&res)
   130  	return res, err
   131  }
   132  
   133  func (l *FeaturedBotLoader) dbKey(arg keybase1.FeaturedBotsArg) libkb.DbKey {
   134  	return libkb.DbKey{
   135  		Typ: libkb.DBFeaturedBots,
   136  		Key: fmt.Sprintf("fb:%d:%d", arg.Limit, arg.Offset),
   137  	}
   138  }
   139  
   140  func (l *FeaturedBotLoader) featuredBotsFromStorage(mctx libkb.MetaContext, arg keybase1.FeaturedBotsArg) (res featuredBotsCache, found bool, err error) {
   141  	dbKey := l.dbKey(arg)
   142  	var cachedData featuredBotsCache
   143  	found, err = mctx.G().GetKVStore().GetInto(&cachedData, dbKey)
   144  	if err != nil || !found {
   145  		return res, false, err
   146  	}
   147  	if !cachedData.isFresh() {
   148  		l.debug(mctx, "featuredBotsFromStorage: data not fresh, ctime: %v", cachedData.Ctime)
   149  		return res, false, nil
   150  	}
   151  	return cachedData, true, nil
   152  }
   153  
   154  func (l *FeaturedBotLoader) storeFeaturedBots(mctx libkb.MetaContext, arg keybase1.FeaturedBotsArg, res keybase1.FeaturedBotsRes) error {
   155  	l.debug(mctx, "storeFeaturedBots: storing %d bots", len(res.Bots))
   156  	dbKey := l.dbKey(arg)
   157  	return mctx.G().GetKVStore().PutObj(dbKey, nil, featuredBotsCache{
   158  		Data:  res,
   159  		Ctime: gregor1.ToTime(time.Now()),
   160  	})
   161  }
   162  
   163  func (l *FeaturedBotLoader) present(mctx libkb.MetaContext, bots []keybase1.FeaturedBot) (res []keybase1.FeaturedBot) {
   164  	res = make([]keybase1.FeaturedBot, len(bots))
   165  	for index, bot := range bots {
   166  		res[index] = bot
   167  		res[index].ExtendedDescriptionRaw = bot.ExtendedDescription
   168  		res[index].ExtendedDescription = utils.PresentDecoratedUserBio(mctx.Ctx(), bot.ExtendedDescription)
   169  	}
   170  	return res
   171  }
   172  
   173  func (l *FeaturedBotLoader) shouldRefresh(cache *featuredBotsCache) bool {
   174  	return cache == nil || time.Since(cache.Ctime.Time()) > refreshLifetime
   175  }
   176  
   177  func (l *FeaturedBotLoader) syncFeaturedBots(mctx libkb.MetaContext, arg keybase1.FeaturedBotsArg, cache *featuredBotsCache) (res keybase1.FeaturedBotsRes, err error) {
   178  	defer mctx.Trace("FeaturedBotLoader: syncFeaturedBots", &err)()
   179  	if !l.shouldRefresh(cache) {
   180  		return res, nil
   181  	}
   182  	res, err = l.featuredBotsFromServer(mctx, arg)
   183  	if err != nil {
   184  		l.debug(mctx, "syncFeaturedBots: failed to load from server: %s", err)
   185  		return res, err
   186  	}
   187  	if cache == nil || !res.Eq(cache.Data) { // only write out data if it changed
   188  		if err := l.storeFeaturedBots(mctx, arg, res); err != nil {
   189  			l.debug(mctx, "syncFeaturedBots: failed to store result: %s", err)
   190  			return res, err
   191  		}
   192  	}
   193  	l.G().NotifyRouter.HandleFeaturedBots(mctx.Ctx(), l.present(mctx, res.Bots), arg.Limit, arg.Offset)
   194  	return res, nil
   195  }
   196  
   197  func (l *FeaturedBotLoader) FeaturedBots(mctx libkb.MetaContext, arg keybase1.FeaturedBotsArg) (res keybase1.FeaturedBotsRes, err error) {
   198  	defer mctx.Trace("FeaturedBotLoader: FeaturedBots", &err)()
   199  	defer func() {
   200  		if err == nil {
   201  			res.Bots = l.present(mctx, res.Bots)
   202  		}
   203  	}()
   204  	if arg.SkipCache {
   205  		return l.syncFeaturedBots(mctx, arg, nil)
   206  	}
   207  	// send up local copy first quickly
   208  	cache, found, err := l.featuredBotsFromStorage(mctx, arg)
   209  	if err != nil {
   210  		l.debug(mctx, "FeaturedBots: failed to load from local storage: %s", err)
   211  	} else if found {
   212  		l.G().NotifyRouter.HandleFeaturedBots(mctx.Ctx(), l.present(mctx, cache.Data.Bots), arg.Limit, arg.Offset)
   213  		go func() {
   214  			mctx = libkb.NewMetaContextBackground(l.G())
   215  			if _, err := l.syncFeaturedBots(mctx, arg, &cache); err != nil {
   216  				l.debug(mctx, "FeaturedBots: unable to fetch from server in background: %v", err)
   217  			}
   218  		}()
   219  		return cache.Data, err
   220  	}
   221  	return l.syncFeaturedBots(mctx, arg, nil)
   222  }
   223  
   224  func (l *FeaturedBotLoader) AllFeaturedBots(mctx libkb.MetaContext, skipCache bool) (res keybase1.FeaturedBotsRes, err error) {
   225  	arg := keybase1.FeaturedBotsArg{
   226  		Limit:     1000,
   227  		Offset:    0,
   228  		SkipCache: skipCache,
   229  	}
   230  	// Limit the number of iterations so a server bug doesn't cause an infinite
   231  	// loop.
   232  	for i := 0; !res.IsLastPage && i < 5; i++ {
   233  		page, err := l.FeaturedBots(mctx, arg)
   234  		if err != nil {
   235  			return res, err
   236  		}
   237  		res.Bots = append(res.Bots, page.Bots...)
   238  		res.IsLastPage = page.IsLastPage
   239  		arg.Offset += arg.Limit
   240  	}
   241  	return res, nil
   242  }