github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/giphy/search.go (about) 1 package giphy 2 3 import ( 4 "crypto/tls" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "strconv" 12 "time" 13 14 "github.com/keybase/client/go/chat/globals" 15 "github.com/keybase/client/go/chat/storage" 16 "github.com/keybase/client/go/chat/types" 17 "github.com/keybase/client/go/libkb" 18 "github.com/keybase/client/go/protocol/chat1" 19 "golang.org/x/net/context/ctxhttp" 20 ) 21 22 const APIHost = "api.giphy.com" 23 const MediaHost = "media.giphy.com" 24 const Host = "giphy.com" 25 const giphyProxy = "https://giphy-proxy.core.keybaseapi.com" 26 27 func getPreferredPreview(mctx libkb.MetaContext, img gifImage) (string, bool, error) { 28 isMobile := mctx.G().IsMobileAppType() 29 if len(img.MP4) == 0 && len(img.URL) == 0 { 30 return "", false, errors.New("no preview") 31 } 32 if len(img.MP4) == 0 { 33 return img.URL, false, nil 34 } 35 if len(img.URL) == 0 { 36 if isMobile { 37 return "", false, errors.New("need gif for mobile") 38 } 39 return img.MP4, true, nil 40 } 41 if isMobile { 42 return img.URL, false, nil 43 } 44 return img.MP4, true, nil 45 } 46 47 func getTargetURL(mctx libkb.MetaContext, images map[string]gifImage) (string, error) { 48 adorn := func(url string, isVideo bool, img gifImage) string { 49 return fmt.Sprintf("%s#height=%s&width=%s&isvideo=%v", url, img.Height, img.Width, isVideo) 50 } 51 for typ, img := range images { 52 if typ != "original" { 53 continue 54 } 55 if len(img.MP4) == 0 && len(img.URL) == 0 { 56 return "", errors.New("no gif target") 57 } 58 if len(img.MP4) == 0 { 59 return adorn(img.URL, false, img), nil 60 } 61 return adorn(img.MP4, true, img), nil 62 } 63 return "", errors.New("no original target found") 64 } 65 66 func formatResponse(mctx libkb.MetaContext, response giphyResponse, srv types.AttachmentURLSrv) (res []chat1.GiphySearchResult) { 67 var err error 68 for _, obj := range response.Data { 69 var searchRes chat1.GiphySearchResult 70 foundPreview := true 71 for typ, img := range obj.Images { 72 select { 73 case <-mctx.Ctx().Done(): 74 return 75 default: 76 } 77 if typ != "fixed_height" { 78 continue 79 } 80 searchRes.PreferredPreviewUrl, searchRes.PreviewIsVideo, err = getPreferredPreview(mctx, img) 81 if err != nil { 82 continue 83 } 84 searchRes.PreviewHeight, err = strconv.Atoi(img.Height) 85 if err != nil { 86 continue 87 } 88 searchRes.PreviewWidth, err = strconv.Atoi(img.Width) 89 if err != nil { 90 continue 91 } 92 searchRes.PreviewUrl = srv.GetGiphyURL(mctx.Ctx(), searchRes.PreferredPreviewUrl) 93 foundPreview = true 94 break 95 } 96 if foundPreview { 97 searchRes.TargetUrl, err = getTargetURL(mctx, obj.Images) 98 if err != nil { 99 continue 100 } 101 res = append(res, searchRes) 102 } 103 } 104 return res 105 } 106 107 func httpClient(mctx libkb.MetaContext, host string) *http.Client { 108 var xprt http.Transport 109 tlsConfig := &tls.Config{ 110 ServerName: host, 111 } 112 xprt.TLSClientConfig = tlsConfig 113 114 env := mctx.G().Env 115 xprt.Proxy = libkb.MakeProxy(env) 116 117 return &http.Client{ 118 Transport: libkb.NewInstrumentedRoundTripper(mctx.G(), 119 func(*http.Request) string { return host + " Giphy" }, libkb.NewClosingRoundTripper(&xprt)), 120 Timeout: 10 * time.Second, 121 } 122 } 123 124 func APIClient(mctx libkb.MetaContext) *http.Client { 125 return httpClient(mctx, APIHost) 126 } 127 128 func AssetClient(mctx libkb.MetaContext) *http.Client { 129 return httpClient(mctx, MediaHost) 130 } 131 132 func WebClient(mctx libkb.MetaContext) *http.Client { 133 return httpClient(mctx, Host) 134 } 135 136 func runAPICall(mctx libkb.MetaContext, endpoint string, srv types.AttachmentURLSrv) (res []chat1.GiphySearchResult, err error) { 137 req, err := http.NewRequest("GET", endpoint, nil) 138 if err != nil { 139 return res, err 140 } 141 req.Host = APIHost 142 143 resp, err := ctxhttp.Do(mctx.Ctx(), APIClient(mctx), req) 144 if err != nil { 145 return res, err 146 } 147 defer resp.Body.Close() 148 dat, err := io.ReadAll(resp.Body) 149 if err != nil { 150 return res, err 151 } 152 var response giphyResponse 153 if err := json.Unmarshal(dat, &response); err != nil { 154 return res, err 155 } 156 return formatResponse(mctx, response, srv), nil 157 } 158 159 func ProxyURL(sourceURL string) (res string, err error) { 160 u, err := url.Parse(sourceURL) 161 if err != nil { 162 return res, err 163 } 164 return fmt.Sprintf("%s%s", giphyProxy, u.Path), nil 165 } 166 167 func Asset(mctx libkb.MetaContext, sourceURL string) (res io.ReadCloser, length int64, err error) { 168 proxyURL, err := ProxyURL(sourceURL) 169 if err != nil { 170 return nil, 0, err 171 } 172 req, err := http.NewRequest("GET", proxyURL, nil) 173 if err != nil { 174 return nil, 0, err 175 } 176 req.Header.Add("Accept", "image/*") 177 req.Host = MediaHost 178 resp, err := ctxhttp.Do(mctx.Ctx(), AssetClient(mctx), req) 179 if err != nil { 180 return nil, 0, err 181 } 182 183 if resp.StatusCode != 200 { 184 return nil, 0, fmt.Errorf("Status %s", resp.Status) 185 } 186 return resp.Body, resp.ContentLength, nil 187 } 188 189 func Search(g *globals.Context, mctx libkb.MetaContext, apiKeySource types.ExternalAPIKeySource, query *string, limit int, 190 srv types.AttachmentURLSrv) (res []chat1.GiphySearchResult, err error) { 191 var endpoint string 192 apiKey, err := apiKeySource.GetKey(mctx.Ctx(), chat1.ExternalAPIKeyTyp_GIPHY) 193 if err != nil { 194 return res, err 195 } 196 if query != nil { 197 endpoint = fmt.Sprintf("%s/v1/gifs/search?api_key=%s&q=%s&limit=%d", giphyProxy, apiKey.Giphy(), 198 url.QueryEscape(*query), limit) 199 return runAPICall(mctx, endpoint, srv) 200 } 201 202 // If we have no query first check the local store for recently used results. 203 recentlyUsedLimit := 7 204 if mctx.G().IsMobileAppType() { 205 recentlyUsedLimit = 3 206 } 207 208 results := storage.NewGiphyStore(g).GiphyResults(mctx.Ctx(), mctx.CurrentUID().ToBytes(), recentlyUsedLimit) 209 // Refresh the local url for any previously cached results. 210 seenPreviewURLs := make(map[string]bool) 211 for i, result := range results { 212 result.PreviewUrl = srv.GetGiphyURL(mctx.Ctx(), result.PreferredPreviewUrl) 213 results[i] = result 214 seenPreviewURLs[result.PreviewUrl] = true 215 } 216 217 if len(results) > limit { 218 results = results[:limit] 219 } else if len(results) < limit { // grab trending if we don't have enough recents 220 limit -= len(results) 221 endpoint = fmt.Sprintf("%s/v1/gifs/trending?api_key=%s&limit=%d", giphyProxy, apiKey.Giphy(), limit) 222 trendingResults, err := runAPICall(mctx, endpoint, srv) 223 if err != nil { 224 return nil, err 225 } 226 // Filter out any results already from the cached response. 227 for _, result := range trendingResults { 228 if !seenPreviewURLs[result.PreviewUrl] { 229 results = append(results, result) 230 seenPreviewURLs[result.PreviewUrl] = true 231 } 232 } 233 } 234 return results, nil 235 }