github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/search/regexp_searcher.go (about)

     1  package search
     2  
     3  import (
     4  	"context"
     5  	"regexp"
     6  
     7  	"github.com/keybase/client/go/chat/globals"
     8  	"github.com/keybase/client/go/chat/types"
     9  	"github.com/keybase/client/go/chat/utils"
    10  	"github.com/keybase/client/go/protocol/chat1"
    11  	"github.com/keybase/client/go/protocol/gregor1"
    12  )
    13  
    14  type RegexpSearcher struct {
    15  	globals.Contextified
    16  	utils.DebugLabeler
    17  
    18  	pageSize int
    19  }
    20  
    21  var _ types.RegexpSearcher = (*RegexpSearcher)(nil)
    22  
    23  func NewRegexpSearcher(g *globals.Context) *RegexpSearcher {
    24  	labeler := utils.NewDebugLabeler(g.ExternalG(), "RegexpSearcher", false)
    25  	return &RegexpSearcher{
    26  		Contextified: globals.NewContextified(g),
    27  		DebugLabeler: labeler,
    28  		pageSize:     defaultPageSize,
    29  	}
    30  }
    31  
    32  func (s *RegexpSearcher) SetPageSize(pageSize int) {
    33  	s.pageSize = pageSize
    34  }
    35  
    36  func (s *RegexpSearcher) Search(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
    37  	queryRe *regexp.Regexp, uiCh chan chat1.ChatSearchHit, opts chat1.SearchOpts) (hits []chat1.ChatSearchHit, msgHits []chat1.MessageUnboxed, err error) {
    38  	defer s.Trace(ctx, &err, "Search")()
    39  	defer func() {
    40  		if uiCh != nil {
    41  			close(uiCh)
    42  		}
    43  	}()
    44  	pagination := opts.InitialPagination
    45  	if pagination == nil {
    46  		pagination = &chat1.Pagination{Num: s.pageSize}
    47  	} else {
    48  		pagination.Num = s.pageSize
    49  	}
    50  	maxHits := opts.MaxHits
    51  	maxMessages := opts.MaxMessages
    52  	beforeContext := opts.BeforeContext
    53  	afterContext := opts.AfterContext
    54  
    55  	if beforeContext >= MaxContext || beforeContext < 0 {
    56  		beforeContext = MaxContext - 1
    57  	}
    58  	if afterContext >= MaxContext || afterContext < 0 {
    59  		afterContext = MaxContext - 1
    60  	}
    61  
    62  	if maxHits > MaxAllowedSearchHits || maxHits <= 0 {
    63  		maxHits = MaxAllowedSearchHits
    64  	}
    65  
    66  	if maxMessages > MaxAllowedSearchMessages || maxMessages <= 0 {
    67  		maxMessages = MaxAllowedSearchMessages
    68  	}
    69  
    70  	// If we have to gather search result context around a pagination boundary,
    71  	// we may have to fetch the next page of the thread
    72  	var prevPage, curPage, nextPage *chat1.ThreadView
    73  
    74  	getNextPage := func() (*chat1.ThreadView, error) {
    75  		thread, err := s.G().ConvSource.Pull(ctx, convID, uid,
    76  			chat1.GetThreadReason_SEARCHER, nil,
    77  			&chat1.GetThreadQuery{
    78  				MarkAsRead: false,
    79  			}, pagination)
    80  		if err != nil {
    81  			return nil, err
    82  		}
    83  		filteredMsgs := []chat1.MessageUnboxed{}
    84  		// Filter out invalid/exploded messages so our search context is
    85  		// correct.
    86  		for _, msg := range thread.Messages {
    87  			if msg.IsValidFull() && msg.IsVisible() {
    88  				filteredMsgs = append(filteredMsgs, msg)
    89  			}
    90  		}
    91  		thread.Messages = filteredMsgs
    92  		return &thread, nil
    93  	}
    94  
    95  	// Returns search context before the search hit, at position `i` in
    96  	// `cur.Messages` possibly fetching and returning a new page of results if
    97  	// we are at a pagination boundary.
    98  	getBeforeMsgs := func(i int, cur, next *chat1.ThreadView) (*chat1.ThreadView, []chat1.MessageUnboxed, error) {
    99  		// context is contained entirely in this page of the thread.
   100  		if i+beforeContext < len(cur.Messages) {
   101  			return next, cur.Messages[i+1 : i+beforeContext+1], nil
   102  		}
   103  		// Get all of the context after our hit index of the current page and
   104  		// fetch a new page if available.
   105  		hitContext := cur.Messages[i+1:]
   106  		if next == nil {
   107  			next, err = getNextPage()
   108  			if err != nil {
   109  				if utils.IsPermanentErr(err) {
   110  					return nil, nil, err
   111  				}
   112  				s.Debug(ctx, "transient search failure: %v", err)
   113  				return nil, nil, nil
   114  			}
   115  		}
   116  		// Get the remaining context from the new current page of the thread.
   117  		remainingContext := beforeContext - len(hitContext)
   118  		if remainingContext > len(next.Messages) {
   119  			remainingContext = len(next.Messages)
   120  		}
   121  		hitContext = append(hitContext, next.Messages[:remainingContext]...)
   122  		return next, hitContext, nil
   123  	}
   124  
   125  	// Returns the search context surrounding a search result at index `i` in
   126  	// `cur.Messages`, possibly using prev if we are at a
   127  	// pagination boundary (since msgs are ordered last to first).
   128  	getAfterMsgs := func(i int, prev, cur *chat1.ThreadView) []chat1.MessageUnboxed {
   129  		// Return context from the current thread only
   130  		if afterContext < i {
   131  			return cur.Messages[i-afterContext : i]
   132  		}
   133  		hitContext := cur.Messages[:i]
   134  		if prev != nil {
   135  			// Get the remaining context from the previous page of the thread.
   136  			remainingContext := len(prev.Messages) - (afterContext - len(hitContext))
   137  			if remainingContext < 0 {
   138  				remainingContext = 0
   139  			}
   140  			hitContext = append(prev.Messages[remainingContext:], hitContext...)
   141  		}
   142  		return hitContext
   143  	}
   144  
   145  	numHits := 0
   146  	numMessages := 0
   147  	for !pagination.Last && numHits < maxHits && numMessages < maxMessages {
   148  		prevPage = curPage
   149  		if nextPage == nil {
   150  			curPage, err = getNextPage()
   151  			if err != nil {
   152  				return nil, nil, err
   153  			} else if curPage == nil {
   154  				break
   155  			}
   156  		} else { // we pre-fetched the next page when retrieving context
   157  			curPage = nextPage
   158  			nextPage = nil
   159  		}
   160  		// update our global pagination so we can correctly fetch the next page.
   161  		pagination = curPage.Pagination
   162  		pagination.Num = s.pageSize
   163  		pagination.Previous = nil
   164  
   165  		for i, msg := range curPage.Messages {
   166  			numMessages++
   167  			if !opts.Matches(msg) {
   168  				continue
   169  			}
   170  			matches := searchMatches(msg, queryRe)
   171  			if len(matches) > 0 {
   172  				numHits++
   173  
   174  				afterMsgs := getAfterMsgs(i, prevPage, curPage)
   175  				newThread, beforeMsgs, err := getBeforeMsgs(i, curPage, nextPage)
   176  				if err != nil {
   177  					return nil, nil, err
   178  				}
   179  				nextPage = newThread
   180  				searchHit := chat1.ChatSearchHit{
   181  					BeforeMessages: getUIMsgs(ctx, s.G(), convID, uid, beforeMsgs),
   182  					HitMessage:     utils.PresentMessageUnboxed(ctx, s.G(), msg, uid, convID),
   183  					AfterMessages:  getUIMsgs(ctx, s.G(), convID, uid, afterMsgs),
   184  					Matches:        matches,
   185  				}
   186  				if uiCh != nil {
   187  					// Stream search hits back to the UI channel
   188  					select {
   189  					case <-ctx.Done():
   190  						return nil, nil, ctx.Err()
   191  					case uiCh <- searchHit:
   192  					}
   193  				}
   194  				hits = append(hits, searchHit)
   195  				msgHits = append(msgHits, msg)
   196  			}
   197  			if numHits >= maxHits || numMessages >= maxMessages {
   198  				break
   199  			}
   200  		}
   201  	}
   202  	return hits, msgHits, nil
   203  }