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