github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/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 }