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  }