github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/chat/commands/giphy.go (about)

     1  package commands
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  
     7  	"github.com/keybase/client/go/chat/giphy"
     8  	"github.com/keybase/client/go/chat/globals"
     9  	"github.com/keybase/client/go/chat/types"
    10  	"github.com/keybase/client/go/libkb"
    11  	"github.com/keybase/client/go/protocol/chat1"
    12  	"github.com/keybase/client/go/protocol/gregor1"
    13  )
    14  
    15  type giphySearcher interface {
    16  	Search(mctx libkb.MetaContext, apiKeySource types.ExternalAPIKeySource, query *string, limit int,
    17  		urlsrv types.AttachmentURLSrv) ([]chat1.GiphySearchResult, error)
    18  }
    19  
    20  type defaultGiphySearcher struct {
    21  	globals.Contextified
    22  }
    23  
    24  func (d defaultGiphySearcher) Search(mctx libkb.MetaContext, apiKeySource types.ExternalAPIKeySource,
    25  	query *string, limit int, urlsrv types.AttachmentURLSrv) ([]chat1.GiphySearchResult, error) {
    26  	return giphy.Search(d.G(), mctx, apiKeySource, query, limit, urlsrv)
    27  }
    28  
    29  type Giphy struct {
    30  	sync.Mutex
    31  	*baseCommand
    32  	shownResults      map[chat1.ConvIDStr]*string
    33  	shownWindow       map[chat1.ConvIDStr]bool
    34  	currentOpCancelFn context.CancelFunc
    35  	currentOpDoneCb   chan struct{}
    36  	searcher          giphySearcher
    37  }
    38  
    39  func NewGiphy(g *globals.Context) *Giphy {
    40  	usage := "Search for and post GIFs"
    41  	return &Giphy{
    42  		baseCommand:  newBaseCommand(g, "giphy", "[search terms]", usage, true),
    43  		shownResults: make(map[chat1.ConvIDStr]*string),
    44  		shownWindow:  make(map[chat1.ConvIDStr]bool),
    45  		searcher: defaultGiphySearcher{
    46  			Contextified: globals.NewContextified(g),
    47  		},
    48  	}
    49  }
    50  
    51  func (s *Giphy) getQuery(text string) *string {
    52  	var query *string
    53  	_, q, err := s.commandAndMessage(text)
    54  	if err != nil {
    55  		return nil
    56  	}
    57  	if len(q) > 0 {
    58  		query = &q
    59  	}
    60  	return query
    61  }
    62  
    63  func (s *Giphy) getLimit() int {
    64  	limit := 25
    65  	if s.G().IsMobileAppType() {
    66  		limit = 10
    67  	}
    68  	return limit
    69  }
    70  
    71  func (s *Giphy) Execute(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
    72  	tlfName, text string, replyTo *chat1.MessageID) (err error) {
    73  	if !s.Match(ctx, text) {
    74  		return ErrInvalidCommand
    75  	}
    76  	results, err := s.searcher.Search(libkb.NewMetaContext(ctx, s.G().ExternalG()),
    77  		s.G().ExternalAPIKeySource, s.getQuery(text), s.getLimit(), s.G().AttachmentURLSrv)
    78  	if err != nil {
    79  		s.Debug(ctx, "Execute: failed to get Giphy results: %s", err)
    80  		return err
    81  	}
    82  	if len(results) == 0 {
    83  		s.Debug(ctx, "Execute: failed to find any results")
    84  		return nil
    85  	}
    86  	res := results[libkb.RandIntn(len(results))]
    87  	_, err = s.G().ChatHelper.SendTextByIDNonblock(ctx, convID, tlfName, res.TargetUrl, nil, replyTo)
    88  	return err
    89  }
    90  
    91  func (s *Giphy) queryEqual(query *string, shown *string) bool {
    92  	if query == nil && shown == nil {
    93  		return true
    94  	} else if query == nil && shown != nil || query != nil && shown == nil {
    95  		return false
    96  	}
    97  	return *query == *shown
    98  }
    99  
   100  func (s *Giphy) Preview(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   101  	tlfName, text string) {
   102  	defer s.Trace(ctx, nil, "Preview")()
   103  	s.Lock()
   104  	if s.currentOpCancelFn != nil {
   105  		s.currentOpCancelFn()
   106  	}
   107  	select {
   108  	case <-s.currentOpDoneCb:
   109  		s.Debug(ctx, "Preview: waiting for previous run to terminate")
   110  	default:
   111  	}
   112  	s.Debug(ctx, "Preview: cleared for takeoff")
   113  	ctx, s.currentOpCancelFn = context.WithCancel(ctx)
   114  	s.currentOpDoneCb = make(chan struct{})
   115  	s.Unlock()
   116  	defer close(s.currentOpDoneCb)
   117  
   118  	if !s.Match(ctx, text) {
   119  		if _, ok := s.shownWindow[convID.ConvIDStr()]; ok {
   120  			// tell UI to clear
   121  			err := s.getChatUI().ChatGiphyToggleResultWindow(ctx, convID, false, false)
   122  			if err != nil {
   123  				s.Debug(ctx, "Preview: error on toggle result: %+v", err)
   124  			}
   125  			delete(s.shownResults, convID.ConvIDStr())
   126  			delete(s.shownWindow, convID.ConvIDStr())
   127  		}
   128  		return
   129  	}
   130  	query := s.getQuery(text)
   131  	if shown, ok := s.shownResults[convID.ConvIDStr()]; ok && s.queryEqual(query, shown) {
   132  		s.Debug(ctx, "Preview: same query given, skipping")
   133  		return
   134  	}
   135  	err := s.getChatUI().ChatGiphyToggleResultWindow(ctx, convID, true, false)
   136  	if err != nil {
   137  		s.Debug(ctx, "Preview: error on toggle result: %+v", err)
   138  	}
   139  
   140  	s.shownWindow[convID.ConvIDStr()] = true
   141  
   142  	results, err := s.searcher.Search(libkb.NewMetaContext(ctx, s.G().ExternalG()),
   143  		s.G().ExternalAPIKeySource, query, s.getLimit(), s.G().AttachmentURLSrv)
   144  	if err != nil {
   145  		s.Debug(ctx, "Preview: failed to get Giphy results: %s", err)
   146  		return
   147  	}
   148  	err = s.getChatUI().ChatGiphySearchResults(ctx, convID, chat1.GiphySearchResults{
   149  		Results:    results,
   150  		GalleryUrl: s.G().AttachmentURLSrv.GetGiphyGalleryURL(ctx, convID, tlfName, results),
   151  	})
   152  	if err != nil {
   153  		s.Debug(ctx, "Preview: error on search results: %+v", err)
   154  		return
   155  	}
   156  
   157  	s.shownResults[convID.ConvIDStr()] = query
   158  }