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  }