github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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, chat1.GetS3ParamsArg{ 98 ConversationID: convID, 99 TempCreds: true, 100 }) 101 if err != nil { 102 return res, err 103 } 104 outboxID, err := storage.NewOutboxID() 105 if err != nil { 106 return res, err 107 } 108 task := attachments.UploadTask{ 109 S3Params: s3params, 110 Filename: filename, 111 FileSize: len, 112 Plaintext: src, 113 S3Signer: p.s3signer, 114 ConversationID: convID, 115 UserID: uid, 116 OutboxID: outboxID, 117 } 118 if res, err = p.store.UploadAsset(ctx, &task, io.Discard); err != nil { 119 return res, err 120 } 121 res.MimeType = contentType 122 res.Metadata = md 123 return res, nil 124 } 125 126 func (p *Packager) assetFromURLWithBody(ctx context.Context, body io.ReadCloser, contentLength int64, 127 url string, uid gregor1.UID, convID chat1.ConversationID, usePreview bool) (res chat1.Asset, err error) { 128 defer body.Close() 129 if contentLength > 0 && contentLength > p.maxAssetSize { 130 return res, fmt.Errorf("asset too large: %d > %d", contentLength, p.maxAssetSize) 131 } 132 dat, err := io.ReadAll(body) 133 if err != nil { 134 return res, err 135 } 136 if int64(len(dat)) > p.maxAssetSize { 137 return res, fmt.Errorf("asset too large: %d > %d", len(dat), p.maxAssetSize) 138 } 139 140 filename := p.assetFilename(url) 141 src := attachments.NewBufReadResetter(dat) 142 pre, err := attachments.PreprocessAsset(ctx, p.G(), p.DebugLabeler, src, filename, 143 types.DummyNativeVideoHelper{}, nil) 144 if err != nil { 145 return res, err 146 } 147 if err := src.Reset(); err != nil { 148 return res, err 149 } 150 151 filename = pre.Filename 152 uploadLen := len(dat) 153 if pre.SrcDat != nil { 154 src = attachments.NewBufReadResetter(pre.SrcDat) 155 uploadLen = len(pre.SrcDat) 156 } 157 uploadPt := src 158 uploadMd := pre.BaseMetadata() 159 uploadContentType := pre.ContentType 160 if usePreview && pre.Preview != nil { 161 uploadPt = attachments.NewBufReadResetter(pre.Preview) 162 uploadLen = len(pre.Preview) 163 uploadMd = pre.PreviewMetadata() 164 uploadContentType = pre.PreviewContentType 165 } else { 166 p.Debug(ctx, "assetFromURL: warning, failed to generate preview for asset, using base") 167 } 168 return p.uploadAsset(ctx, uid, convID, uploadPt, filename, int64(uploadLen), uploadMd, uploadContentType) 169 } 170 171 func (p *Packager) uploadVideo(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 172 video chat1.UnfurlVideo) (res chat1.Asset, err error) { 173 body, len, err := p.assetBodyAndLength(ctx, video.Url) 174 if err != nil { 175 return res, err 176 } 177 defer body.Close() 178 return p.uploadVideoWithBody(ctx, uid, convID, body, len, video) 179 } 180 181 func (p *Packager) uploadVideoWithBody(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 182 body io.ReadCloser, len int64, video chat1.UnfurlVideo) (res chat1.Asset, err error) { 183 dat, err := io.ReadAll(body) 184 if err != nil { 185 return res, err 186 } 187 return p.uploadAsset(ctx, uid, convID, attachments.NewBufReadResetter(dat), "video.mp4", 188 len, chat1.NewAssetMetadataWithVideo(chat1.AssetMetadataVideo{ 189 Width: video.Width, 190 Height: video.Height, 191 DurationMs: 1, 192 }), video.MimeType) 193 } 194 195 func (p *Packager) packageGeneric(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 196 raw chat1.UnfurlRaw) (res chat1.Unfurl, err error) { 197 g := chat1.UnfurlGeneric{ 198 Title: raw.Generic().Title, 199 Url: raw.Generic().Url, 200 SiteName: raw.Generic().SiteName, 201 PublishTime: raw.Generic().PublishTime, 202 Description: raw.Generic().Description, 203 } 204 if raw.Generic().Video != nil { 205 asset, err := p.uploadVideo(ctx, uid, convID, *raw.Generic().Video) 206 if err != nil { 207 p.Debug(ctx, "packageGeneric: failed to package video asset: %s", err) 208 } 209 g.Image = &asset 210 } 211 if g.Image == nil && raw.Generic().ImageUrl != nil { 212 asset, err := p.assetFromURL(ctx, *raw.Generic().ImageUrl, uid, convID, true) 213 if err != nil { 214 p.Debug(ctx, "packageGeneric: failed to get image asset URL: %s", err) 215 } else { 216 g.Image = &asset 217 } 218 } 219 if raw.Generic().FaviconUrl != nil { 220 asset, err := p.assetFromURL(ctx, *raw.Generic().FaviconUrl, uid, convID, true) 221 if err != nil { 222 p.Debug(ctx, "packageGeneric: failed to get favicon asset URL: %s", err) 223 } else { 224 g.Favicon = &asset 225 } 226 } 227 return chat1.NewUnfurlWithGeneric(g), nil 228 } 229 230 func (p *Packager) packageGiphy(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 231 raw chat1.UnfurlRaw) (res chat1.Unfurl, err error) { 232 var g chat1.UnfurlGiphy 233 var imgBody io.ReadCloser 234 var imgLength int64 235 if raw.Giphy().ImageUrl != nil { 236 imgBody, imgLength, err = giphy.Asset(libkb.NewMetaContext(ctx, p.G().ExternalG()), 237 *raw.Giphy().ImageUrl) 238 if err != nil { 239 p.Debug(ctx, "Package: failed to get body specs for giphy image: %s", err) 240 return res, err 241 } 242 defer imgBody.Close() 243 } 244 if raw.Giphy().Video != nil { 245 // If we found a video, then let's see if it is smaller than the image, if so we will 246 // set it (which means it will get used by the frontend) 247 vidBody, vidLength, err := giphy.Asset(libkb.NewMetaContext(ctx, p.G().ExternalG()), 248 raw.Giphy().Video.Url) 249 if err == nil && (imgLength == 0 || vidLength < imgLength) && vidLength < p.maxAssetSize { 250 p.Debug(ctx, "Package: found video: len: %d", vidLength) 251 defer vidBody.Close() 252 asset, err := p.uploadVideoWithBody(ctx, uid, convID, vidBody, vidLength, 253 *raw.Giphy().Video) 254 if err != nil { 255 p.Debug(ctx, "Package: failed to get video asset URL: %s", err) 256 } else { 257 g.Video = &asset 258 } 259 } else if err != nil { 260 p.Debug(ctx, "Package: failed to get video specs: %s", err) 261 } else { 262 defer vidBody.Close() 263 p.Debug(ctx, "Package: not selecting video: %d(video) > %d(image)", vidLength, imgLength) 264 } 265 } 266 if g.Video == nil && raw.Giphy().ImageUrl != nil { 267 // Only grab the image if we didn't get a video 268 asset, err := p.assetFromURLWithBody(ctx, imgBody, imgLength, *raw.Giphy().ImageUrl, uid, 269 convID, true) 270 if err != nil { 271 // if we don't get the image, then just bail out of here 272 p.Debug(ctx, "Package: failed to get image asset URL: %s", err) 273 return res, errors.New("image not available for giphy unfurl") 274 } 275 g.Image = &asset 276 } 277 if raw.Giphy().FaviconUrl != nil { 278 if asset, err := p.assetFromURL(ctx, *raw.Giphy().FaviconUrl, uid, convID, true); err != nil { 279 p.Debug(ctx, "Package: failed to get favicon asset URL: %s", err) 280 } else { 281 g.Favicon = &asset 282 } 283 } 284 return chat1.NewUnfurlWithGiphy(g), nil 285 } 286 287 func (p *Packager) packageMaps(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 288 raw chat1.UnfurlRaw) (res chat1.Unfurl, err error) { 289 mapsRaw := raw.Maps() 290 g := chat1.UnfurlGeneric{ 291 Title: mapsRaw.Title, 292 Url: mapsRaw.Url, 293 SiteName: mapsRaw.SiteName, 294 Description: &mapsRaw.Description, 295 } 296 297 // load user avatar for fancy maps 298 username := p.G().ExternalG().GetEnv().GetUsername().String() 299 avatarReader, _, err := avatars.GetBorderedCircleAvatar(ctx, p.G(), username, 48, 8, 8) 300 if err != nil { 301 return res, err 302 } 303 defer avatarReader.Close() 304 305 // load map 306 var reader io.ReadCloser 307 var length int64 308 var isDone bool 309 mapsURL := mapsRaw.ImageUrl 310 locReader, _, err := maps.MapReaderFromURL(ctx, p.G(), mapsURL) 311 if err != nil { 312 return res, err 313 } 314 defer locReader.Close() 315 if mapsRaw.HistoryImageUrl != nil { 316 liveReader, _, err := maps.MapReaderFromURL(ctx, p.G(), *mapsRaw.HistoryImageUrl) 317 if err != nil { 318 return res, err 319 } 320 defer liveReader.Close() 321 if reader, length, err = maps.DecorateMap(ctx, avatarReader, liveReader); err != nil { 322 return res, err 323 } 324 isDone = mapsRaw.LiveLocationDone 325 } else { 326 if reader, length, err = maps.DecorateMap(ctx, avatarReader, locReader); err != nil { 327 return res, err 328 } 329 isDone = false 330 } 331 asset, err := p.assetFromURLWithBody(ctx, reader, length, mapsURL, uid, convID, true) 332 if err != nil { 333 p.Debug(ctx, "Package: failed to get maps asset URL: %s", err) 334 return res, errors.New("image not available for maps unfurl") 335 } 336 g.Image = &asset 337 g.MapInfo = &chat1.UnfurlGenericMapInfo{ 338 Coord: mapsRaw.Coord, 339 LiveLocationEndTime: mapsRaw.LiveLocationEndTime, 340 IsLiveLocationDone: isDone, 341 Time: mapsRaw.Time, 342 } 343 return chat1.NewUnfurlWithGeneric(g), nil 344 } 345 346 func (p *Packager) cacheKey(uid gregor1.UID, convID chat1.ConversationID, raw chat1.UnfurlRaw) string { 347 url := raw.GetUrl() 348 if url == "" { 349 return "" 350 } 351 typ, err := raw.UnfurlType() 352 if err != nil { 353 return "" 354 } 355 return fmt.Sprintf("%s-%s-%s-%s", uid, convID, url, typ) 356 } 357 358 func (p *Packager) Package(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 359 raw chat1.UnfurlRaw) (res chat1.Unfurl, err error) { 360 defer p.Trace(ctx, &err, "Package")() 361 362 cacheKey := p.cacheKey(uid, convID, raw) 363 if item, valid := p.cache.get(cacheKey); cacheKey != "" && valid { 364 p.Debug(ctx, "Package: using cached value") 365 return item.data.(chat1.Unfurl), nil 366 } 367 defer func() { 368 if cacheKey != "" && err == nil { 369 p.cache.put(cacheKey, res) 370 } 371 }() 372 373 typ, err := raw.UnfurlType() 374 if err != nil { 375 return res, err 376 } 377 switch typ { 378 case chat1.UnfurlType_GENERIC: 379 return p.packageGeneric(ctx, uid, convID, raw) 380 case chat1.UnfurlType_GIPHY: 381 return p.packageGiphy(ctx, uid, convID, raw) 382 case chat1.UnfurlType_MAPS: 383 return p.packageMaps(ctx, uid, convID, raw) 384 default: 385 return res, errors.New("not implemented") 386 } 387 }