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 }