github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/unfurl/unfurler.go (about) 1 package unfurl 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net" 8 "net/url" 9 "sync" 10 11 "github.com/keybase/client/go/chat/attachments" 12 "github.com/keybase/client/go/chat/globals" 13 "github.com/keybase/client/go/chat/s3" 14 "github.com/keybase/client/go/chat/storage" 15 "github.com/keybase/client/go/chat/types" 16 "github.com/keybase/client/go/chat/utils" 17 "github.com/keybase/client/go/libkb" 18 "github.com/keybase/client/go/protocol/chat1" 19 "github.com/keybase/client/go/protocol/gregor1" 20 "github.com/keybase/clockwork" 21 ) 22 23 type unfurlPermanentError struct { 24 msg string 25 } 26 27 func newUnfurlPermanentError(msg string) *unfurlPermanentError { 28 return &unfurlPermanentError{ 29 msg: msg, 30 } 31 } 32 33 func (e *unfurlPermanentError) Error() string { 34 return e.msg 35 } 36 37 type unfurlTask struct { 38 UID gregor1.UID 39 ConvID chat1.ConversationID 40 URL string 41 Result *chat1.Unfurl 42 } 43 44 type UnfurlMessageSender interface { 45 SendUnfurlNonblock(ctx context.Context, convID chat1.ConversationID, 46 msg chat1.MessagePlaintext, clientPrev chat1.MessageID, outboxID chat1.OutboxID) (chat1.OutboxID, error) 47 } 48 49 type Unfurler struct { 50 sync.Mutex 51 prefetchLock sync.Mutex 52 globals.Contextified 53 utils.DebugLabeler 54 55 unfurlMap map[string]bool 56 extractor *Extractor 57 scraper *Scraper 58 packager *Packager 59 settings *Settings 60 sender UnfurlMessageSender 61 62 // testing 63 unfurlCh chan *chat1.Unfurl 64 retryCh chan struct{} 65 } 66 67 var _ types.Unfurler = (*Unfurler)(nil) 68 69 func NewUnfurler(g *globals.Context, store attachments.Store, s3signer s3.Signer, 70 storage types.UserConversationBackedStorage, sender UnfurlMessageSender, ri func() chat1.RemoteInterface) *Unfurler { 71 extractor := NewExtractor(g) 72 scraper := NewScraper(g) 73 packager := NewPackager(g, store, s3signer, ri) 74 settings := NewSettings(g, storage) 75 return &Unfurler{ 76 Contextified: globals.NewContextified(g), 77 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Unfurler", false), 78 unfurlMap: make(map[string]bool), 79 extractor: extractor, 80 scraper: scraper, 81 packager: packager, 82 settings: settings, 83 sender: sender, 84 } 85 } 86 87 func (u *Unfurler) SetClock(clock clockwork.Clock) { 88 u.scraper.cache.setClock(clock) 89 u.packager.cache.setClock(clock) 90 } 91 92 func (u *Unfurler) SetTestingRetryCh(ch chan struct{}) { 93 u.retryCh = ch 94 } 95 96 func (u *Unfurler) SetTestingUnfurlCh(ch chan *chat1.Unfurl) { 97 u.unfurlCh = ch 98 } 99 100 func (u *Unfurler) Complete(ctx context.Context, outboxID chat1.OutboxID) { 101 defer u.Trace(ctx, nil, "Complete(%s)", outboxID)() 102 if err := u.G().GetKVStore().Delete(u.taskKey(outboxID)); err != nil { 103 u.Debug(ctx, "Complete: failed to delete task: %s", err) 104 } 105 if err := u.G().GetKVStore().Delete(u.statusKey(outboxID)); err != nil { 106 u.Debug(ctx, "Complete: failed to delete status: %s", err) 107 } 108 } 109 110 func (u *Unfurler) statusKey(outboxID chat1.OutboxID) libkb.DbKey { 111 return libkb.DbKey{ 112 Typ: libkb.DBUnfurler, 113 Key: fmt.Sprintf("s|%s", outboxID), 114 } 115 } 116 117 func (u *Unfurler) taskKey(outboxID chat1.OutboxID) libkb.DbKey { 118 return libkb.DbKey{ 119 Typ: libkb.DBUnfurler, 120 Key: fmt.Sprintf("t|%s", outboxID), 121 } 122 } 123 124 func (u *Unfurler) Status(ctx context.Context, outboxID chat1.OutboxID) (status types.UnfurlerTaskStatus, res *chat1.UnfurlResult, err error) { 125 defer u.Trace(ctx, nil, "Status(%s)", outboxID)() 126 task, err := u.getTask(ctx, outboxID) 127 if err != nil { 128 u.Debug(ctx, "Status: error finding task: outboxID: %s err: %s", outboxID, err) 129 return types.UnfurlerTaskStatusFailed, nil, err 130 } 131 found, err := u.G().GetKVStore().GetInto(&status, u.statusKey(outboxID)) 132 if err != nil { 133 return types.UnfurlerTaskStatusFailed, nil, err 134 } 135 if !found { 136 u.Debug(ctx, "Status: failed to find status, using unfurling: outboxID: %s", outboxID) 137 status = types.UnfurlerTaskStatusUnfurling 138 } 139 if task.Result != nil { 140 return status, &chat1.UnfurlResult{ 141 Unfurl: *task.Result, 142 Url: task.URL, 143 }, nil 144 } 145 return status, nil, nil 146 } 147 148 func (u *Unfurler) Retry(ctx context.Context, outboxID chat1.OutboxID) { 149 defer u.Trace(ctx, nil, "Retry(%s)", outboxID)() 150 u.unfurl(ctx, outboxID) 151 if u.retryCh != nil { 152 u.retryCh <- struct{}{} 153 } 154 } 155 156 func (u *Unfurler) extractURLs(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 157 msg chat1.MessageUnboxed) (res []ExtractorHit) { 158 if !msg.IsValid() { 159 return nil 160 } 161 body := msg.Valid().MessageBody 162 typ, err := body.MessageType() 163 if err != nil { 164 return nil 165 } 166 switch typ { 167 case chat1.MessageType_TEXT: 168 hits, err := u.extractor.Extract(ctx, uid, convID, msg.GetMessageID(), body.Text().Body, u.settings) 169 if err != nil { 170 u.Debug(ctx, "extractURLs: failed to extract: %s", err) 171 return nil 172 } 173 return hits 174 default: 175 // Nothing to do for other message types. 176 } 177 return nil 178 } 179 180 func (u *Unfurler) getTask(ctx context.Context, outboxID chat1.OutboxID) (res unfurlTask, err error) { 181 found, err := u.G().GetKVStore().GetInto(&res, u.taskKey(outboxID)) 182 if err != nil { 183 return res, err 184 } 185 if !found { 186 return res, libkb.NotFoundError{} 187 } 188 return res, nil 189 } 190 191 func (u *Unfurler) saveTask(ctx context.Context, outboxID chat1.OutboxID, uid gregor1.UID, 192 convID chat1.ConversationID, url string) error { 193 return u.G().GetKVStore().PutObj(u.taskKey(outboxID), nil, unfurlTask{ 194 UID: uid, 195 ConvID: convID, 196 URL: url, 197 }) 198 } 199 200 func (u *Unfurler) setTaskResult(ctx context.Context, outboxID chat1.OutboxID, unfurl chat1.Unfurl) error { 201 task, err := u.getTask(ctx, outboxID) 202 if err != nil { 203 return err 204 } 205 task.Result = &unfurl 206 return u.G().GetKVStore().PutObj(u.taskKey(outboxID), nil, task) 207 } 208 209 func (u *Unfurler) setStatus(ctx context.Context, outboxID chat1.OutboxID, status types.UnfurlerTaskStatus) error { 210 return u.G().GetKVStore().PutObj(u.statusKey(outboxID), nil, status) 211 } 212 213 func (u *Unfurler) makeBaseUnfurlMessage(ctx context.Context, fromMsg chat1.MessageUnboxed, outboxID chat1.OutboxID) (msg chat1.MessagePlaintext, err error) { 214 if !fromMsg.IsValid() { 215 return msg, errors.New("invalid message") 216 } 217 tlfName := fromMsg.Valid().ClientHeader.TlfName 218 public := fromMsg.Valid().ClientHeader.TlfPublic 219 msg = chat1.MessagePlaintext{ 220 ClientHeader: chat1.MessageClientHeader{ 221 MessageType: chat1.MessageType_UNFURL, 222 TlfName: tlfName, 223 TlfPublic: public, 224 OutboxID: &outboxID, 225 Supersedes: fromMsg.GetMessageID(), 226 }, 227 MessageBody: chat1.NewMessageBodyWithUnfurl(chat1.MessageUnfurl{ 228 MessageID: fromMsg.GetMessageID(), 229 }), 230 } 231 return msg, nil 232 } 233 234 func (u *Unfurler) UnfurlAndSend(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 235 msg chat1.MessageUnboxed) { 236 defer u.Trace(ctx, nil, "UnfurlAndSend")() 237 // early out for errors 238 if !msg.IsValid() { 239 u.Debug(ctx, "UnfurlAndSend: skipping invalid") 240 return 241 } 242 // get URL hits 243 hits := u.extractURLs(ctx, uid, convID, msg) 244 if len(hits) == 0 { 245 return 246 } 247 // get a map for all the URLs we have already unfurled 248 prevUnfurled := make(map[string]bool) 249 for _, u := range msg.Valid().Unfurls { 250 prevUnfurled[u.Url] = true 251 } 252 // for each hit, either prompt the user for action, or generate a new message 253 for _, hit := range hits { 254 if prevUnfurled[hit.URL] { 255 u.Debug(ctx, "UnfurlAndSend: skipping prev unfurled") 256 continue 257 } 258 prevUnfurled[hit.URL] = true // only one action per unique URL 259 switch hit.Typ { 260 case ExtractorHitPrompt: 261 domain, err := GetDomain(hit.URL) 262 if err != nil { 263 u.Debug(ctx, "UnfurlAndSend: error getting domain for prompt: %s", err) 264 continue 265 } 266 u.G().ActivityNotifier.PromptUnfurl(ctx, uid, convID, msg.GetMessageID(), domain) 267 case ExtractorHitUnfurl: 268 outboxID := storage.GetOutboxIDFromURL(hit.URL, convID, msg) 269 if _, err := u.getTask(ctx, outboxID); err == nil { 270 u.Debug(ctx, "UnfurlAndSend: skipping URL hit, task exists: outboxID: %s", outboxID) 271 continue 272 } 273 unfurlMsg, err := u.makeBaseUnfurlMessage(ctx, msg, outboxID) 274 if err != nil { 275 u.Debug(ctx, "UnfurlAndSend: failed to make message: %s", err) 276 continue 277 } 278 u.Debug(ctx, "UnfurlAndSend: saving task for outboxID: %s", outboxID) 279 if err := u.saveTask(ctx, outboxID, uid, convID, hit.URL); err != nil { 280 u.Debug(ctx, "UnfurlAndSend: failed to save task: %s", err) 281 continue 282 } 283 // Unfurl in background and send the message (requires nonblocking sender) 284 u.unfurl(ctx, outboxID) 285 u.Debug(ctx, "UnfurlAndSend: sending message for outboxID: %s", outboxID) 286 if _, err := u.sender.SendUnfurlNonblock(ctx, convID, unfurlMsg, msg.GetMessageID(), outboxID); err != nil { 287 u.Debug(ctx, "UnfurlAndSend: failed to send message: %s", err) 288 } 289 default: 290 u.Debug(ctx, "UnfurlAndSend: unknown hit typ: %v", hit.Typ) 291 } 292 } 293 } 294 295 // Prefetch attempts to parse hits out of `msgText` and scrape/package the 296 // unfurl so the result is cached. 297 func (u *Unfurler) Prefetch(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 298 msgText string) (numPrefetched int) { 299 u.prefetchLock.Lock() 300 defer u.prefetchLock.Unlock() 301 defer u.Trace(ctx, nil, "Prefetch")() 302 303 hits, err := u.extractor.Extract(ctx, uid, convID, 0, msgText, u.settings) 304 if err != nil { 305 u.Debug(ctx, "Prefetch: failed to extract: %s", err) 306 return 0 307 } else if len(hits) == 0 { 308 return 0 309 } 310 311 prevUnfurled := make(map[string]bool) 312 // for each hit that is already whitelisted try to prefetch the result to 313 // populate the message cache. 314 for _, hit := range hits { 315 if prevUnfurled[hit.URL] { 316 u.Debug(ctx, "Prefetch: skipping prev unfurled") 317 continue 318 } 319 prevUnfurled[hit.URL] = true // only one action per unique URL 320 if hit.Typ == ExtractorHitUnfurl { 321 if _, err := u.scrapeAndPackage(ctx, uid, convID, hit.URL); err != nil { 322 u.Debug(ctx, "Prefetch: unable to scrapeAndPackge: %s", err) 323 } else { 324 numPrefetched++ 325 } 326 } 327 } 328 return numPrefetched 329 } 330 331 func (u *Unfurler) checkAndSetUnfurling(ctx context.Context, outboxID chat1.OutboxID) (inprogress bool) { 332 u.Lock() 333 defer u.Unlock() 334 if u.unfurlMap[outboxID.String()] { 335 return true 336 } 337 u.unfurlMap[outboxID.String()] = true 338 return false 339 } 340 341 func (u *Unfurler) doneUnfurling(outboxID chat1.OutboxID) { 342 u.Lock() 343 defer u.Unlock() 344 delete(u.unfurlMap, outboxID.String()) 345 } 346 347 func (u *Unfurler) detectPermError(err error) bool { 348 switch e := err.(type) { 349 case *net.DNSError: 350 return !e.Temporary() //nolint 351 case *url.Error: 352 return !e.Temporary() 353 case *unfurlPermanentError: 354 return true 355 } 356 return err.Error() == "Not Found" 357 } 358 359 func (u *Unfurler) testingSendUnfurl(unfurl *chat1.Unfurl) { 360 if u.unfurlCh != nil { 361 u.unfurlCh <- unfurl 362 } 363 } 364 365 func (u *Unfurler) scrapeAndPackage(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 366 url string) (unfurl chat1.Unfurl, err error) { 367 unfurlRaw, err := u.scraper.Scrape(ctx, url, nil) 368 if err != nil { 369 u.Debug(ctx, "unfurl: failed to scrape: <error msg suppressed> (%T)", err) 370 return unfurl, err 371 } 372 packaged, err := u.packager.Package(ctx, uid, convID, unfurlRaw) 373 if err != nil { 374 u.Debug(ctx, "unfurl: failed to package: %s", err) 375 return unfurl, err 376 } 377 return packaged, err 378 } 379 380 func (u *Unfurler) unfurl(ctx context.Context, outboxID chat1.OutboxID) { 381 defer u.Trace(ctx, nil, "unfurl(%s)", outboxID)() 382 if u.checkAndSetUnfurling(ctx, outboxID) { 383 u.Debug(ctx, "unfurl: already unfurling outboxID: %s", outboxID) 384 return 385 } 386 ctx = libkb.CopyTagsToBackground(ctx) 387 f := func(ctx context.Context) (unfurl *chat1.Unfurl, err error) { 388 defer func() { u.testingSendUnfurl(unfurl) }() 389 defer u.doneUnfurling(outboxID) 390 defer func() { 391 if err != nil { 392 status := types.UnfurlerTaskStatusFailed 393 if u.detectPermError(err) { 394 status = types.UnfurlerTaskStatusPermFailed 395 } 396 if err := u.setStatus(ctx, outboxID, status); err != nil { 397 u.Debug(ctx, "unfurl: failed to set failed status: %s", err) 398 } 399 } else { 400 // if it worked, then force Deliverer to run and send our message 401 u.G().MessageDeliverer.ForceDeliverLoop(ctx) 402 } 403 }() 404 task, err := u.getTask(ctx, outboxID) 405 if err != nil { 406 u.Debug(ctx, "unfurl: failed to get task: %s", err) 407 return nil, err 408 } 409 if err := u.setStatus(ctx, outboxID, types.UnfurlerTaskStatusUnfurling); err != nil { 410 u.Debug(ctx, "unfurl: failed to set status: %s", err) 411 } 412 packaged, err := u.scrapeAndPackage(ctx, task.UID, task.ConvID, task.URL) 413 if err != nil { 414 return nil, err 415 } 416 unfurl = new(chat1.Unfurl) 417 *unfurl = packaged 418 if err := u.setTaskResult(ctx, outboxID, *unfurl); err != nil { 419 u.Debug(ctx, "unfurl: failed to set task result: %s", err) 420 return nil, err 421 } 422 if err := u.setStatus(ctx, outboxID, types.UnfurlerTaskStatusSuccess); err != nil { 423 u.Debug(ctx, "unfurl: failed to set task status: %s", err) 424 return nil, err 425 } 426 return unfurl, nil 427 } 428 go func() { _, _ = f(ctx) }() 429 } 430 431 func (u *Unfurler) GetSettings(ctx context.Context, uid gregor1.UID) (res chat1.UnfurlSettings, err error) { 432 defer u.Trace(ctx, nil, "GetSettings")() 433 return u.settings.Get(ctx, uid) 434 } 435 436 func (u *Unfurler) WhitelistAdd(ctx context.Context, uid gregor1.UID, domain string) (err error) { 437 defer u.Trace(ctx, nil, "WhitelistAdd")() 438 return u.settings.WhitelistAdd(ctx, uid, domain) 439 } 440 441 func (u *Unfurler) WhitelistRemove(ctx context.Context, uid gregor1.UID, domain string) (err error) { 442 defer u.Trace(ctx, nil, "WhitelistRemove")() 443 return u.settings.WhitelistRemove(ctx, uid, domain) 444 } 445 446 func (u *Unfurler) WhitelistAddExemption(ctx context.Context, uid gregor1.UID, 447 exemption types.WhitelistExemption) { 448 defer u.Trace(ctx, nil, "WhitelistAddExemption")() 449 u.extractor.AddWhitelistExemption(ctx, uid, exemption) 450 } 451 452 func (u *Unfurler) SetMode(ctx context.Context, uid gregor1.UID, mode chat1.UnfurlMode) (err error) { 453 defer u.Trace(ctx, nil, "SetMode")() 454 return u.settings.SetMode(ctx, uid, mode) 455 } 456 457 func (u *Unfurler) SetSettings(ctx context.Context, uid gregor1.UID, settings chat1.UnfurlSettings) (err error) { 458 defer u.Trace(ctx, nil, "SetSettings")() 459 return u.settings.Set(ctx, uid, settings) 460 }