github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/attachments/preprocess.go (about)

     1  package attachments
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/keybase/client/go/chat/globals"
    13  	"github.com/keybase/client/go/libkb"
    14  
    15  	"github.com/keybase/client/go/chat/types"
    16  	"github.com/keybase/client/go/chat/utils"
    17  
    18  	"github.com/keybase/client/go/protocol/chat1"
    19  	"golang.org/x/net/context"
    20  )
    21  
    22  type Dimension struct {
    23  	Width  int `json:"width"`
    24  	Height int `json:"height"`
    25  }
    26  
    27  func (d *Dimension) Empty() bool {
    28  	return d.Width == 0 && d.Height == 0
    29  }
    30  
    31  func (d *Dimension) Encode() string {
    32  	if d.Width == 0 && d.Height == 0 {
    33  		return ""
    34  	}
    35  	enc, err := json.Marshal(d)
    36  	if err != nil {
    37  		return ""
    38  	}
    39  	return string(enc)
    40  }
    41  
    42  type Preprocess struct {
    43  	ContentType        string
    44  	Preview            []byte
    45  	PreviewContentType string
    46  	BaseDim            *Dimension
    47  	BaseDurationMs     int
    48  	BaseIsAudio        bool
    49  	PreviewDim         *Dimension
    50  	PreviewAudioAmps   []float64
    51  	PreviewDurationMs  int
    52  }
    53  
    54  func (p *Preprocess) BaseMetadata() chat1.AssetMetadata {
    55  	if p.BaseDim == nil || p.BaseDim.Empty() {
    56  		return chat1.AssetMetadata{}
    57  	}
    58  	if p.BaseDurationMs > 0 {
    59  		return chat1.NewAssetMetadataWithVideo(chat1.AssetMetadataVideo{
    60  			Width:      p.BaseDim.Width,
    61  			Height:     p.BaseDim.Height,
    62  			DurationMs: p.BaseDurationMs,
    63  			IsAudio:    p.BaseIsAudio,
    64  		})
    65  	}
    66  	return chat1.NewAssetMetadataWithImage(chat1.AssetMetadataImage{
    67  		Width:  p.BaseDim.Width,
    68  		Height: p.BaseDim.Height,
    69  	})
    70  }
    71  
    72  func (p *Preprocess) PreviewMetadata() chat1.AssetMetadata {
    73  	if p.PreviewDim == nil || p.PreviewDim.Empty() {
    74  		return chat1.AssetMetadata{}
    75  	}
    76  	if p.PreviewDurationMs > 0 {
    77  		return chat1.NewAssetMetadataWithVideo(chat1.AssetMetadataVideo{
    78  			Width:      p.PreviewDim.Width,
    79  			Height:     p.PreviewDim.Height,
    80  			DurationMs: p.PreviewDurationMs,
    81  		})
    82  	}
    83  	return chat1.NewAssetMetadataWithImage(chat1.AssetMetadataImage{
    84  		Width:     p.PreviewDim.Width,
    85  		Height:    p.PreviewDim.Height,
    86  		AudioAmps: p.PreviewAudioAmps,
    87  	})
    88  }
    89  
    90  func (p *Preprocess) Export(getLocation func() *chat1.PreviewLocation) (res chat1.MakePreviewRes, err error) {
    91  	res = chat1.MakePreviewRes{
    92  		MimeType: p.ContentType,
    93  		Location: getLocation(),
    94  	}
    95  	if p.PreviewContentType != "" {
    96  		res.PreviewMimeType = &p.PreviewContentType
    97  	}
    98  	md := p.PreviewMetadata()
    99  	var empty chat1.AssetMetadata
   100  	if md != empty {
   101  		res.Metadata = &md
   102  	}
   103  	baseMd := p.BaseMetadata()
   104  	if baseMd != empty {
   105  		res.BaseMetadata = &baseMd
   106  	}
   107  	return res, nil
   108  }
   109  
   110  func processCallerPreview(ctx context.Context, g *globals.Context, callerPreview chat1.MakePreviewRes) (p Preprocess, err error) {
   111  	ltyp, err := callerPreview.Location.Ltyp()
   112  	if err != nil {
   113  		return p, err
   114  	}
   115  	switch ltyp {
   116  	case chat1.PreviewLocationTyp_BYTES:
   117  		source := callerPreview.Location.Bytes()
   118  		p.Preview = make([]byte, len(source))
   119  		copy(p.Preview, source)
   120  	case chat1.PreviewLocationTyp_FILE:
   121  		f, err := os.Open(callerPreview.Location.File())
   122  		if err != nil {
   123  			return p, err
   124  		}
   125  		defer f.Close()
   126  		if p.Preview, err = io.ReadAll(f); err != nil {
   127  			return p, err
   128  		}
   129  	case chat1.PreviewLocationTyp_URL:
   130  		resp, err := libkb.ProxyHTTPGet(g.ExternalG(), g.Env, callerPreview.Location.Url(), "PreviewLocation")
   131  		if err != nil {
   132  			return p, err
   133  		}
   134  		defer resp.Body.Close()
   135  		if p.Preview, err = io.ReadAll(resp.Body); err != nil {
   136  			return p, err
   137  		}
   138  	default:
   139  		return p, fmt.Errorf("unknown preview location: %v", ltyp)
   140  	}
   141  	p.ContentType = callerPreview.MimeType
   142  	if callerPreview.PreviewMimeType != nil {
   143  		p.PreviewContentType = *callerPreview.PreviewMimeType
   144  	}
   145  	if callerPreview.Metadata != nil {
   146  		typ, err := callerPreview.Metadata.AssetType()
   147  		if err != nil {
   148  			return p, err
   149  		}
   150  		switch typ {
   151  		case chat1.AssetMetadataType_IMAGE:
   152  			p.PreviewDim = &Dimension{
   153  				Width:  callerPreview.Metadata.Image().Width,
   154  				Height: callerPreview.Metadata.Image().Height,
   155  			}
   156  			p.PreviewAudioAmps = callerPreview.Metadata.Image().AudioAmps
   157  		case chat1.AssetMetadataType_VIDEO:
   158  			p.PreviewDurationMs = callerPreview.Metadata.Video().DurationMs
   159  			p.PreviewDim = &Dimension{
   160  				Width:  callerPreview.Metadata.Video().Width,
   161  				Height: callerPreview.Metadata.Video().Height,
   162  			}
   163  		}
   164  	}
   165  	if callerPreview.BaseMetadata != nil {
   166  		typ, err := callerPreview.BaseMetadata.AssetType()
   167  		if err != nil {
   168  			return p, err
   169  		}
   170  		switch typ {
   171  		case chat1.AssetMetadataType_IMAGE:
   172  			p.BaseDim = &Dimension{
   173  				Width:  callerPreview.BaseMetadata.Image().Width,
   174  				Height: callerPreview.BaseMetadata.Image().Height,
   175  			}
   176  		case chat1.AssetMetadataType_VIDEO:
   177  			p.BaseDurationMs = callerPreview.BaseMetadata.Video().DurationMs
   178  			p.BaseIsAudio = callerPreview.BaseMetadata.Video().IsAudio
   179  			p.BaseDim = &Dimension{
   180  				Width:  callerPreview.BaseMetadata.Video().Width,
   181  				Height: callerPreview.BaseMetadata.Video().Height,
   182  			}
   183  		}
   184  	}
   185  	return p, nil
   186  }
   187  
   188  func DetectMIMEType(ctx context.Context, src ReadResetter, filename string) (res string, err error) {
   189  	head := make([]byte, 512)
   190  	_, err = io.ReadFull(src, head)
   191  	switch err {
   192  	case nil:
   193  	case io.EOF, io.ErrUnexpectedEOF:
   194  		return "", nil
   195  	default:
   196  		return res, err
   197  	}
   198  
   199  	res = http.DetectContentType(head)
   200  	if err = src.Reset(); err != nil {
   201  		return res, err
   202  	}
   203  	// MIME type detection failed us, try using an extension map
   204  	if res == "application/octet-stream" {
   205  		ext := strings.ToLower(filepath.Ext(filename))
   206  		if typ, ok := mimeTypes[ext]; ok {
   207  			res = typ
   208  		}
   209  	}
   210  	return res, nil
   211  }
   212  
   213  func PreprocessAsset(ctx context.Context, g *globals.Context, log utils.DebugLabeler, src ReadResetter, filename string,
   214  	nvh types.NativeVideoHelper, callerPreview *chat1.MakePreviewRes) (p Preprocess, err error) {
   215  	if callerPreview != nil && callerPreview.Location != nil {
   216  		log.Debug(ctx, "preprocessAsset: caller provided preview, using that")
   217  		if p, err = processCallerPreview(ctx, g, *callerPreview); err != nil {
   218  			log.Debug(ctx, "preprocessAsset: failed to process caller preview, making fresh one: %s", err)
   219  		} else {
   220  			return p, nil
   221  		}
   222  	}
   223  	defer func() {
   224  		err := src.Reset()
   225  		if err != nil {
   226  			log.Debug(ctx, "preprocessAsset: reset failed: %+v", err)
   227  		}
   228  	}()
   229  
   230  	if p.ContentType, err = DetectMIMEType(ctx, src, filename); err != nil {
   231  		return p, err
   232  	}
   233  	log.Debug(ctx, "preprocessAsset: detected attachment content type %s", p.ContentType)
   234  	previewRes, err := Preview(ctx, log, src, p.ContentType, filename, nvh)
   235  	if err != nil {
   236  		log.Debug(ctx, "preprocessAsset: error making preview: %s", err)
   237  		return p, err
   238  	}
   239  	if previewRes != nil {
   240  		log.Debug(ctx, "preprocessAsset: made preview for attachment asset: content type: %s",
   241  			previewRes.ContentType)
   242  		p.Preview = previewRes.Source
   243  		p.PreviewContentType = previewRes.ContentType
   244  		if previewRes.BaseWidth > 0 || previewRes.BaseHeight > 0 {
   245  			p.BaseDim = &Dimension{Width: previewRes.BaseWidth, Height: previewRes.BaseHeight}
   246  		}
   247  		if previewRes.PreviewWidth > 0 || previewRes.PreviewHeight > 0 {
   248  			p.PreviewDim = &Dimension{Width: previewRes.PreviewWidth, Height: previewRes.PreviewHeight}
   249  		}
   250  		p.BaseDurationMs = previewRes.BaseDurationMs
   251  		p.PreviewDurationMs = previewRes.PreviewDurationMs
   252  	}
   253  
   254  	return p, nil
   255  }