github.com/keybase/client/go@v0.0.0-20240520164431-4f512a4c85a3/chat/unfurl/packager.go (about)

     1  package unfurl
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"strings"
    10  
    11  	"github.com/keybase/client/go/avatars"
    12  	"github.com/keybase/client/go/chat/globals"
    13  	"github.com/keybase/client/go/libkb"
    14  
    15  	"github.com/keybase/client/go/chat/maps"
    16  
    17  	"github.com/keybase/client/go/chat/attachments"
    18  	"github.com/keybase/client/go/chat/giphy"
    19  	"github.com/keybase/client/go/chat/s3"
    20  	"github.com/keybase/client/go/chat/storage"
    21  	"github.com/keybase/client/go/chat/types"
    22  
    23  	"github.com/keybase/client/go/chat/utils"
    24  	"github.com/keybase/client/go/protocol/chat1"
    25  	"github.com/keybase/client/go/protocol/gregor1"
    26  )
    27  
    28  type Packager struct {
    29  	globals.Contextified
    30  	utils.DebugLabeler
    31  
    32  	cache        *unfurlCache
    33  	ri           func() chat1.RemoteInterface
    34  	store        attachments.Store
    35  	s3signer     s3.Signer
    36  	maxAssetSize int64
    37  }
    38  
    39  func NewPackager(g *globals.Context, store attachments.Store, s3signer s3.Signer,
    40  	ri func() chat1.RemoteInterface) *Packager {
    41  	return &Packager{
    42  		Contextified: globals.NewContextified(g),
    43  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Packager", false),
    44  		cache:        newUnfurlCache(),
    45  		store:        store,
    46  		ri:           ri,
    47  		s3signer:     s3signer,
    48  		maxAssetSize: 10000000,
    49  	}
    50  }
    51  
    52  func (p *Packager) assetFilename(url string) string {
    53  	toks := strings.Split(url, "/")
    54  	if len(toks) > 0 {
    55  		return toks[len(toks)-1]
    56  	}
    57  	return "unknown.jpg"
    58  }
    59  
    60  func (p *Packager) assetBodyAndLength(ctx context.Context, url string) (body io.ReadCloser, size int64, err error) {
    61  	client := libkb.ProxyHTTPClient(p.G().ExternalG(), p.G().Env, "UnfurlPackager")
    62  	req, err := http.NewRequest(http.MethodGet, url, nil)
    63  	if err != nil {
    64  		return nil, 0, err
    65  	}
    66  	req.Header.Add("User-Agent", libkb.UserAgent)
    67  
    68  	resp, err := client.Do(req)
    69  	if err != nil {
    70  		return body, size, err
    71  	}
    72  	if resp.StatusCode != 200 {
    73  		return nil, 0, fmt.Errorf("Status %s", resp.Status)
    74  	}
    75  	return resp.Body, resp.ContentLength, nil
    76  }
    77  
    78  func (p *Packager) assetFromURL(ctx context.Context, url string, uid gregor1.UID,
    79  	convID chat1.ConversationID, usePreview bool) (res chat1.Asset, err error) {
    80  	body, contentLength, err := p.assetBodyAndLength(ctx, url)
    81  	if err != nil {
    82  		return res, err
    83  	}
    84  	return p.assetFromURLWithBody(ctx, body, contentLength, url, uid, convID, usePreview)
    85  }
    86  
    87  func (p *Packager) uploadAsset(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
    88  	src *attachments.BufReadResetter, filename string, len int64, md chat1.AssetMetadata, contentType string) (res chat1.Asset, err error) {
    89  	atyp, err := md.AssetType()
    90  	if err != nil {
    91  		return res, err
    92  	}
    93  	if atyp != chat1.AssetMetadataType_IMAGE && atyp != chat1.AssetMetadataType_VIDEO {
    94  		return res, fmt.Errorf("invalid asset for unfurl package: %v mime: %s", atyp, contentType)
    95  	}
    96  
    97  	s3params, err := p.ri().GetS3Params(ctx, convID)
    98  	if err != nil {
    99  		return res, err
   100  	}
   101  	outboxID, err := storage.NewOutboxID()
   102  	if err != nil {
   103  		return res, err
   104  	}
   105  	task := attachments.UploadTask{
   106  		S3Params:       s3params,
   107  		Filename:       filename,
   108  		FileSize:       len,
   109  		Plaintext:      src,
   110  		S3Signer:       p.s3signer,
   111  		ConversationID: convID,
   112  		UserID:         uid,
   113  		OutboxID:       outboxID,
   114  	}
   115  	if res, err = p.store.UploadAsset(ctx, &task, io.Discard); err != nil {
   116  		return res, err
   117  	}
   118  	res.MimeType = contentType
   119  	res.Metadata = md
   120  	return res, nil
   121  }
   122  
   123  func (p *Packager) assetFromURLWithBody(ctx context.Context, body io.ReadCloser, contentLength int64,
   124  	url string, uid gregor1.UID, convID chat1.ConversationID, usePreview bool) (res chat1.Asset, err error) {
   125  	defer body.Close()
   126  	if contentLength > 0 && contentLength > p.maxAssetSize {
   127  		return res, fmt.Errorf("asset too large: %d > %d", contentLength, p.maxAssetSize)
   128  	}
   129  	dat, err := io.ReadAll(body)
   130  	if err != nil {
   131  		return res, err
   132  	}
   133  	if int64(len(dat)) > p.maxAssetSize {
   134  		return res, fmt.Errorf("asset too large: %d > %d", len(dat), p.maxAssetSize)
   135  	}
   136  
   137  	filename := p.assetFilename(url)
   138  	src := attachments.NewBufReadResetter(dat)
   139  	pre, err := attachments.PreprocessAsset(ctx, p.G(), p.DebugLabeler, src, filename,
   140  		types.DummyNativeVideoHelper{}, nil)
   141  	if err != nil {
   142  		return res, err
   143  	}
   144  	if err := src.Reset(); err != nil {
   145  		return res, err
   146  	}
   147  
   148  	filename = pre.Filename
   149  	uploadLen := len(dat)
   150  	if pre.SrcDat != nil {
   151  		src = attachments.NewBufReadResetter(pre.SrcDat)
   152  		uploadLen = len(pre.SrcDat)
   153  	}
   154  	uploadPt := src
   155  	uploadMd := pre.BaseMetadata()
   156  	uploadContentType := pre.ContentType
   157  	if usePreview && pre.Preview != nil {
   158  		uploadPt = attachments.NewBufReadResetter(pre.Preview)
   159  		uploadLen = len(pre.Preview)
   160  		uploadMd = pre.PreviewMetadata()
   161  		uploadContentType = pre.PreviewContentType
   162  	} else {
   163  		p.Debug(ctx, "assetFromURL: warning, failed to generate preview for asset, using base")
   164  	}
   165  	return p.uploadAsset(ctx, uid, convID, uploadPt, filename, int64(uploadLen), uploadMd, uploadContentType)
   166  }
   167  
   168  func (p *Packager) uploadVideo(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   169  	video chat1.UnfurlVideo) (res chat1.Asset, err error) {
   170  	body, len, err := p.assetBodyAndLength(ctx, video.Url)
   171  	if err != nil {
   172  		return res, err
   173  	}
   174  	defer body.Close()
   175  	return p.uploadVideoWithBody(ctx, uid, convID, body, len, video)
   176  }
   177  
   178  func (p *Packager) uploadVideoWithBody(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   179  	body io.ReadCloser, len int64, video chat1.UnfurlVideo) (res chat1.Asset, err error) {
   180  	dat, err := io.ReadAll(body)
   181  	if err != nil {
   182  		return res, err
   183  	}
   184  	return p.uploadAsset(ctx, uid, convID, attachments.NewBufReadResetter(dat), "video.mp4",
   185  		len, chat1.NewAssetMetadataWithVideo(chat1.AssetMetadataVideo{
   186  			Width:      video.Width,
   187  			Height:     video.Height,
   188  			DurationMs: 1,
   189  		}), video.MimeType)
   190  }
   191  
   192  func (p *Packager) packageGeneric(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   193  	raw chat1.UnfurlRaw) (res chat1.Unfurl, err error) {
   194  	g := chat1.UnfurlGeneric{
   195  		Title:       raw.Generic().Title,
   196  		Url:         raw.Generic().Url,
   197  		SiteName:    raw.Generic().SiteName,
   198  		PublishTime: raw.Generic().PublishTime,
   199  		Description: raw.Generic().Description,
   200  	}
   201  	if raw.Generic().Video != nil {
   202  		asset, err := p.uploadVideo(ctx, uid, convID, *raw.Generic().Video)
   203  		if err != nil {
   204  			p.Debug(ctx, "packageGeneric: failed to package video asset: %s", err)
   205  		}
   206  		g.Image = &asset
   207  	}
   208  	if g.Image == nil && raw.Generic().ImageUrl != nil {
   209  		asset, err := p.assetFromURL(ctx, *raw.Generic().ImageUrl, uid, convID, true)
   210  		if err != nil {
   211  			p.Debug(ctx, "packageGeneric: failed to get image asset URL: %s", err)
   212  		} else {
   213  			g.Image = &asset
   214  		}
   215  	}
   216  	if raw.Generic().FaviconUrl != nil {
   217  		asset, err := p.assetFromURL(ctx, *raw.Generic().FaviconUrl, uid, convID, true)
   218  		if err != nil {
   219  			p.Debug(ctx, "packageGeneric: failed to get favicon asset URL: %s", err)
   220  		} else {
   221  			g.Favicon = &asset
   222  		}
   223  	}
   224  	return chat1.NewUnfurlWithGeneric(g), nil
   225  }
   226  
   227  func (p *Packager) packageGiphy(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   228  	raw chat1.UnfurlRaw) (res chat1.Unfurl, err error) {
   229  	var g chat1.UnfurlGiphy
   230  	var imgBody io.ReadCloser
   231  	var imgLength int64
   232  	if raw.Giphy().ImageUrl != nil {
   233  		imgBody, imgLength, err = giphy.Asset(libkb.NewMetaContext(ctx, p.G().ExternalG()),
   234  			*raw.Giphy().ImageUrl)
   235  		if err != nil {
   236  			p.Debug(ctx, "Package: failed to get body specs for giphy image: %s", err)
   237  			return res, err
   238  		}
   239  		defer imgBody.Close()
   240  	}
   241  	if raw.Giphy().Video != nil {
   242  		// If we found a video, then let's see if it is smaller than the image, if so we will
   243  		// set it (which means it will get used by the frontend)
   244  		vidBody, vidLength, err := giphy.Asset(libkb.NewMetaContext(ctx, p.G().ExternalG()),
   245  			raw.Giphy().Video.Url)
   246  		if err == nil && (imgLength == 0 || vidLength < imgLength) && vidLength < p.maxAssetSize {
   247  			p.Debug(ctx, "Package: found video: len: %d", vidLength)
   248  			defer vidBody.Close()
   249  			asset, err := p.uploadVideoWithBody(ctx, uid, convID, vidBody, vidLength,
   250  				*raw.Giphy().Video)
   251  			if err != nil {
   252  				p.Debug(ctx, "Package: failed to get video asset URL: %s", err)
   253  			} else {
   254  				g.Video = &asset
   255  			}
   256  		} else if err != nil {
   257  			p.Debug(ctx, "Package: failed to get video specs: %s", err)
   258  		} else {
   259  			defer vidBody.Close()
   260  			p.Debug(ctx, "Package: not selecting video: %d(video) > %d(image)", vidLength, imgLength)
   261  		}
   262  	}
   263  	if g.Video == nil && raw.Giphy().ImageUrl != nil {
   264  		// Only grab the image if we didn't get a video
   265  		asset, err := p.assetFromURLWithBody(ctx, imgBody, imgLength, *raw.Giphy().ImageUrl, uid,
   266  			convID, true)
   267  		if err != nil {
   268  			// if we don't get the image, then just bail out of here
   269  			p.Debug(ctx, "Package: failed to get image asset URL: %s", err)
   270  			return res, errors.New("image not available for giphy unfurl")
   271  		}
   272  		g.Image = &asset
   273  	}
   274  	if raw.Giphy().FaviconUrl != nil {
   275  		if asset, err := p.assetFromURL(ctx, *raw.Giphy().FaviconUrl, uid, convID, true); err != nil {
   276  			p.Debug(ctx, "Package: failed to get favicon asset URL: %s", err)
   277  		} else {
   278  			g.Favicon = &asset
   279  		}
   280  	}
   281  	return chat1.NewUnfurlWithGiphy(g), nil
   282  }
   283  
   284  func (p *Packager) packageMaps(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   285  	raw chat1.UnfurlRaw) (res chat1.Unfurl, err error) {
   286  	mapsRaw := raw.Maps()
   287  	g := chat1.UnfurlGeneric{
   288  		Title:       mapsRaw.Title,
   289  		Url:         mapsRaw.Url,
   290  		SiteName:    mapsRaw.SiteName,
   291  		Description: &mapsRaw.Description,
   292  	}
   293  
   294  	// load user avatar for fancy maps
   295  	username := p.G().ExternalG().GetEnv().GetUsername().String()
   296  	avatarReader, _, err := avatars.GetBorderedCircleAvatar(ctx, p.G(), username, 48, 8, 8)
   297  	if err != nil {
   298  		return res, err
   299  	}
   300  	defer avatarReader.Close()
   301  
   302  	// load map
   303  	var reader io.ReadCloser
   304  	var length int64
   305  	var isDone bool
   306  	mapsURL := mapsRaw.ImageUrl
   307  	locReader, _, err := maps.MapReaderFromURL(ctx, p.G(), mapsURL)
   308  	if err != nil {
   309  		return res, err
   310  	}
   311  	defer locReader.Close()
   312  	if mapsRaw.HistoryImageUrl != nil {
   313  		liveReader, _, err := maps.MapReaderFromURL(ctx, p.G(), *mapsRaw.HistoryImageUrl)
   314  		if err != nil {
   315  			return res, err
   316  		}
   317  		defer liveReader.Close()
   318  		if reader, length, err = maps.DecorateMap(ctx, avatarReader, liveReader); err != nil {
   319  			return res, err
   320  		}
   321  		isDone = mapsRaw.LiveLocationDone
   322  	} else {
   323  		if reader, length, err = maps.DecorateMap(ctx, avatarReader, locReader); err != nil {
   324  			return res, err
   325  		}
   326  		isDone = false
   327  	}
   328  	asset, err := p.assetFromURLWithBody(ctx, reader, length, mapsURL, uid, convID, true)
   329  	if err != nil {
   330  		p.Debug(ctx, "Package: failed to get maps asset URL: %s", err)
   331  		return res, errors.New("image not available for maps unfurl")
   332  	}
   333  	g.Image = &asset
   334  	g.MapInfo = &chat1.UnfurlGenericMapInfo{
   335  		Coord:               mapsRaw.Coord,
   336  		LiveLocationEndTime: mapsRaw.LiveLocationEndTime,
   337  		IsLiveLocationDone:  isDone,
   338  		Time:                mapsRaw.Time,
   339  	}
   340  	return chat1.NewUnfurlWithGeneric(g), nil
   341  }
   342  
   343  func (p *Packager) cacheKey(uid gregor1.UID, convID chat1.ConversationID, raw chat1.UnfurlRaw) string {
   344  	url := raw.GetUrl()
   345  	if url == "" {
   346  		return ""
   347  	}
   348  	typ, err := raw.UnfurlType()
   349  	if err != nil {
   350  		return ""
   351  	}
   352  	return fmt.Sprintf("%s-%s-%s-%s", uid, convID, url, typ)
   353  }
   354  
   355  func (p *Packager) Package(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   356  	raw chat1.UnfurlRaw) (res chat1.Unfurl, err error) {
   357  	defer p.Trace(ctx, &err, "Package")()
   358  
   359  	cacheKey := p.cacheKey(uid, convID, raw)
   360  	if item, valid := p.cache.get(cacheKey); cacheKey != "" && valid {
   361  		p.Debug(ctx, "Package: using cached value")
   362  		return item.data.(chat1.Unfurl), nil
   363  	}
   364  	defer func() {
   365  		if cacheKey != "" && err == nil {
   366  			p.cache.put(cacheKey, res)
   367  		}
   368  	}()
   369  
   370  	typ, err := raw.UnfurlType()
   371  	if err != nil {
   372  		return res, err
   373  	}
   374  	switch typ {
   375  	case chat1.UnfurlType_GENERIC:
   376  		return p.packageGeneric(ctx, uid, convID, raw)
   377  	case chat1.UnfurlType_GIPHY:
   378  		return p.packageGiphy(ctx, uid, convID, raw)
   379  	case chat1.UnfurlType_MAPS:
   380  		return p.packageMaps(ctx, uid, convID, raw)
   381  	default:
   382  		return res, errors.New("not implemented")
   383  	}
   384  }