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

     1  package wallet
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  
     8  	"github.com/keybase/client/go/chat/utils"
     9  )
    10  
    11  var txPattern = regexp.MustCompile(
    12  	utils.ServiceDecorationPrefix +
    13  		// Have a + in front
    14  		`\+` +
    15  		// Stellar decimal amount
    16  		`(\d+\.?\d*|\d*\.?\d+)` +
    17  		// Currency code
    18  		`([A-Za-z]{2,6})` +
    19  		// At sign and username, optional for direct messages
    20  		`(?:@((?:[a-zA-Z0-9]+_?)+))?` +
    21  		// Sentinel group for advancing the scan
    22  		`()` +
    23  		// Must be followed by a nice character or the end of the string.
    24  		`(?:[\s)\]}:;.,!?"']|\z)`,
    25  )
    26  
    27  var maxAmountLength = 100
    28  var maxUsernameLength = 16
    29  var maxTxsPerMessage = 3000
    30  
    31  type ChatTxCandidate struct {
    32  	Amount       string
    33  	CurrencyCode string
    34  	Username     *string
    35  	Full         string
    36  	Position     []int
    37  }
    38  
    39  func FindChatTxCandidates(xs string) []ChatTxCandidate {
    40  	// A string that does not appear in the candidate regex so we don't get
    41  	// false positives from concatenations.
    42  	replaced := utils.ReplaceQuotedSubstrings(xs, false)
    43  
    44  	buf := replaced // buf is a suffix of replaced
    45  	bufOffset := 0  // buf[0] is replaced[bufOffset]
    46  
    47  	// Munch matches off the front of buf.
    48  	// Can't use FindAllStringSubmatchIndex (emphasis on All) because
    49  	// adjacent matches of txPattern can overlap.
    50  	// For example: "+1xlm +2xlm" -> "+1xlm ", " +2xlm"
    51  	var matches []ChatTxCandidate
    52  	for i := 0; i < maxTxsPerMessage; i++ {
    53  		rawIndices := txPattern.FindStringSubmatchIndex(buf)
    54  		if rawIndices == nil {
    55  			break
    56  		}
    57  		group := func(n int) (s string, startIndex, endIndex int) {
    58  			startIndex, endIndex = rawIndices[2*n], rawIndices[2*n+1]
    59  			if startIndex >= 0 {
    60  				return buf[startIndex:endIndex], startIndex, endIndex
    61  			}
    62  			return "", startIndex, endIndex
    63  		}
    64  		amount, amountStart, _ := group(1)
    65  		_, _, nextIndex := group(4)
    66  		if amount != "0" {
    67  			currencyCode, _, ccEnd := group(2)
    68  			currencyCode = strings.ToUpper(currencyCode)
    69  			var atSign string
    70  			endIndex := ccEnd
    71  			username, _, usernameEndIndex := group(3)
    72  			if len(username) > 0 {
    73  				atSign = "@"
    74  				endIndex = usernameEndIndex
    75  			}
    76  			full := fmt.Sprintf("+%s%s%s%s", amount, currencyCode, atSign, username)
    77  			if len(amount) <= maxAmountLength && len(username) <= maxUsernameLength {
    78  				var txUsername *string
    79  				if username == "" {
    80  					txUsername = nil
    81  				} else {
    82  					txUsername = &username
    83  				}
    84  				matches = append(matches, ChatTxCandidate{
    85  					Full:         full,
    86  					Amount:       amount,
    87  					CurrencyCode: currencyCode,
    88  					Username:     txUsername,
    89  					Position:     []int{amountStart - 1 + bufOffset, endIndex + bufOffset},
    90  				})
    91  			}
    92  		}
    93  		if nextIndex == -1 || nextIndex > len(buf) {
    94  			// should never happen
    95  			return nil
    96  		}
    97  		buf = buf[nextIndex:]
    98  		bufOffset += nextIndex
    99  	}
   100  	return matches
   101  }