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

     1  package attachments
     2  
     3  import (
     4  	"sort"
     5  
     6  	"github.com/keybase/client/go/chat/globals"
     7  	"github.com/keybase/client/go/chat/utils"
     8  	"github.com/keybase/client/go/protocol/chat1"
     9  	"github.com/keybase/client/go/protocol/gregor1"
    10  	"golang.org/x/net/context"
    11  	"mvdan.cc/xurls/v2"
    12  )
    13  
    14  type NextMessageOptions struct {
    15  	BackInTime  bool
    16  	MessageType chat1.MessageType
    17  	AssetTypes  []chat1.AssetMetadataType
    18  	UnfurlTypes []chat1.UnfurlType
    19  	FilterLinks bool
    20  }
    21  
    22  type Gallery struct {
    23  	globals.Contextified
    24  	utils.DebugLabeler
    25  
    26  	PrevStride, NextStride int
    27  }
    28  
    29  func NewGallery(g *globals.Context) *Gallery {
    30  	return &Gallery{
    31  		Contextified: globals.NewContextified(g),
    32  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Attachments.Gallery", false),
    33  		NextStride:   5,
    34  		PrevStride:   50,
    35  	}
    36  }
    37  
    38  func (g *Gallery) eligibleNextMessage(msg chat1.MessageUnboxed, typMap map[chat1.MessageType]bool,
    39  	assetMap map[chat1.AssetMetadataType]bool, unfurlMap map[chat1.UnfurlType]bool) bool {
    40  	if !msg.IsValid() {
    41  		return false
    42  	}
    43  	body := msg.Valid().MessageBody
    44  	typ, err := body.MessageType()
    45  	if err != nil {
    46  		return false
    47  	}
    48  	if !typMap[typ] {
    49  		return false
    50  	}
    51  	switch typ {
    52  	case chat1.MessageType_ATTACHMENT:
    53  		md := body.Attachment().Object.Metadata
    54  		atyp, err := md.AssetType()
    55  		if err != nil {
    56  			return false
    57  		}
    58  		if len(assetMap) > 0 && !assetMap[atyp] {
    59  			return false
    60  		}
    61  	case chat1.MessageType_UNFURL:
    62  		unfurl := body.Unfurl().Unfurl.Unfurl
    63  		typ, err := unfurl.UnfurlType()
    64  		if err != nil {
    65  			return false
    66  		}
    67  		if len(unfurlMap) > 0 && !unfurlMap[typ] {
    68  			return false
    69  		}
    70  	}
    71  	return true
    72  }
    73  
    74  var linkRegexp = xurls.Strict()
    75  
    76  func (g *Gallery) searchForLinks(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
    77  	msgID chat1.MessageID, num int, uiCh chan chat1.UIMessage) (res []chat1.MessageUnboxed, last bool, err error) {
    78  	var hitCh chan chat1.ChatSearchHit
    79  	if uiCh != nil {
    80  		hitCh = make(chan chat1.ChatSearchHit)
    81  		doneCh := make(chan struct{})
    82  		defer func() { <-doneCh }()
    83  		go func() {
    84  			for hit := range hitCh {
    85  				uiCh <- hit.HitMessage
    86  			}
    87  			close(doneCh)
    88  		}()
    89  	}
    90  	idcontrol := &chat1.MessageIDControl{
    91  		Pivot: &msgID,
    92  		Mode:  chat1.MessageIDControlMode_OLDERMESSAGES,
    93  	}
    94  	if _, res, err = g.G().RegexpSearcher.Search(ctx, uid, convID, linkRegexp, hitCh, chat1.SearchOpts{
    95  		InitialPagination: utils.MessageIDControlToPagination(ctx, g.DebugLabeler, idcontrol, nil),
    96  		MaxHits:           num,
    97  	}); err != nil {
    98  		return res, false, err
    99  	}
   100  	return res, len(res) < num, nil
   101  }
   102  
   103  func (g *Gallery) NextMessage(ctx context.Context, uid gregor1.UID,
   104  	convID chat1.ConversationID, msgID chat1.MessageID, opts NextMessageOptions) (res *chat1.MessageUnboxed, last bool, err error) {
   105  	msgs, last, err := g.NextMessages(ctx, uid, convID, msgID, 1, opts, nil)
   106  	if err != nil {
   107  		return res, false, err
   108  	}
   109  	if len(msgs) == 0 {
   110  		return nil, true, nil
   111  	}
   112  	return &msgs[0], last, nil
   113  }
   114  
   115  func (g *Gallery) makeMaps(opts NextMessageOptions) (typMap map[chat1.MessageType]bool,
   116  	assetMap map[chat1.AssetMetadataType]bool, unfurlMap map[chat1.UnfurlType]bool) {
   117  	typMap = make(map[chat1.MessageType]bool)
   118  	assetMap = make(map[chat1.AssetMetadataType]bool)
   119  	unfurlMap = make(map[chat1.UnfurlType]bool)
   120  	typMap[opts.MessageType] = true
   121  	for _, atyp := range opts.AssetTypes {
   122  		assetMap[atyp] = true
   123  	}
   124  	for _, utyp := range opts.UnfurlTypes {
   125  		unfurlMap[utyp] = true
   126  	}
   127  	return typMap, assetMap, unfurlMap
   128  }
   129  
   130  func (g *Gallery) getUnfurlHost(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   131  	msg chat1.MessageUnboxed) (res chat1.MessageUnboxed, err error) {
   132  	if !msg.IsValid() {
   133  		return msg, nil
   134  	}
   135  	if !msg.Valid().MessageBody.IsType(chat1.MessageType_UNFURL) {
   136  		return msg, nil
   137  	}
   138  	hostMsgID := msg.Valid().MessageBody.Unfurl().MessageID
   139  	return g.G().ChatHelper.GetMessage(ctx, uid, convID, hostMsgID, true, nil)
   140  }
   141  
   142  func (g *Gallery) NextMessages(ctx context.Context, uid gregor1.UID,
   143  	convID chat1.ConversationID, msgID chat1.MessageID, num int, opts NextMessageOptions,
   144  	uiCh chan chat1.UIMessage) (res []chat1.MessageUnboxed, last bool, err error) {
   145  	defer g.Trace(ctx, &err, "NextMessages")()
   146  	defer func() {
   147  		if uiCh != nil {
   148  			close(uiCh)
   149  		}
   150  	}()
   151  	var reverseFn func(chat1.ThreadView) []chat1.MessageUnboxed
   152  	var nextPageFn func(*chat1.Pagination) *chat1.Pagination
   153  	pivot := msgID
   154  	mode := chat1.MessageIDControlMode_NEWERMESSAGES
   155  	if opts.BackInTime {
   156  		mode = chat1.MessageIDControlMode_OLDERMESSAGES
   157  	}
   158  	if opts.MessageType == chat1.MessageType_NONE {
   159  		opts.MessageType = chat1.MessageType_ATTACHMENT
   160  	} else if opts.MessageType == chat1.MessageType_TEXT && opts.FilterLinks {
   161  		return g.searchForLinks(ctx, uid, convID, msgID, num, uiCh)
   162  	}
   163  	typMap, assetMap, unfurlMap := g.makeMaps(opts)
   164  	pagination := utils.MessageIDControlToPagination(ctx, g.DebugLabeler, &chat1.MessageIDControl{
   165  		Pivot: &pivot,
   166  		Mode:  mode,
   167  	}, nil)
   168  	if opts.BackInTime {
   169  		reverseFn = func(tv chat1.ThreadView) []chat1.MessageUnboxed {
   170  			return tv.Messages
   171  		}
   172  		nextPageFn = func(p *chat1.Pagination) (res *chat1.Pagination) {
   173  			res = p
   174  			res.Num = g.NextStride
   175  			res.Previous = nil
   176  			return res
   177  		}
   178  		pagination.Num = g.NextStride
   179  	} else {
   180  		reverseFn = func(tv chat1.ThreadView) (res []chat1.MessageUnboxed) {
   181  			res = make([]chat1.MessageUnboxed, len(tv.Messages))
   182  			copy(res, tv.Messages)
   183  			sort.Sort(sort.Reverse(utils.ByMsgUnboxedMsgID(res)))
   184  			return res
   185  		}
   186  		nextPageFn = func(p *chat1.Pagination) (res *chat1.Pagination) {
   187  			pivot += chat1.MessageID(g.PrevStride)
   188  			return utils.MessageIDControlToPagination(ctx, g.DebugLabeler, &chat1.MessageIDControl{
   189  				Pivot: &pivot,
   190  				Num:   g.PrevStride,
   191  				Mode:  chat1.MessageIDControlMode_NEWERMESSAGES,
   192  			}, nil)
   193  		}
   194  		pagination.Num = g.PrevStride
   195  	}
   196  
   197  	for {
   198  		select {
   199  		case <-ctx.Done():
   200  			return res, false, ctx.Err()
   201  		default:
   202  		}
   203  		g.Debug(ctx, "NextMessage: starting scan: p: %s pivot: %d", pagination, pivot)
   204  		tv, err := g.G().ConvSource.Pull(ctx, convID, uid, chat1.GetThreadReason_GENERAL, nil,
   205  			&chat1.GetThreadQuery{
   206  				MessageTypes: []chat1.MessageType{opts.MessageType},
   207  			}, pagination)
   208  		if err != nil {
   209  			return res, false, err
   210  		}
   211  		messages := reverseFn(tv)
   212  		for _, m := range messages {
   213  			if !g.eligibleNextMessage(m, typMap, assetMap, unfurlMap) {
   214  				continue
   215  			}
   216  			if m, err = g.getUnfurlHost(ctx, uid, convID, m); err != nil {
   217  				return res, false, err
   218  			}
   219  			res = append(res, m)
   220  			if uiCh != nil {
   221  				uiCh <- utils.PresentMessageUnboxed(ctx, g.G(), m, uid, convID)
   222  			}
   223  			if len(res) >= num {
   224  				g.Debug(ctx, "NextMessages: stopping on satisfied")
   225  				return res, false, nil
   226  			}
   227  		}
   228  		g.Debug(ctx, "NextMessages: still need more (%d < %d): len: %d", len(res), num, len(tv.Messages))
   229  		if tv.Pagination == nil || tv.Pagination.Last {
   230  			g.Debug(ctx, "NextMessages: stopping on last page")
   231  			break
   232  		}
   233  		pagination = nextPageFn(tv.Pagination)
   234  	}
   235  	return res, true, nil
   236  }