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

     1  package wallet
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/keybase/client/go/chat/globals"
    11  	"github.com/keybase/client/go/chat/types"
    12  	"github.com/keybase/client/go/chat/utils"
    13  	"github.com/keybase/client/go/libkb"
    14  	"github.com/keybase/client/go/protocol/chat1"
    15  	"github.com/keybase/client/go/protocol/gregor1"
    16  	"github.com/keybase/client/go/protocol/keybase1"
    17  )
    18  
    19  type Sender struct {
    20  	globals.Contextified
    21  	utils.DebugLabeler
    22  }
    23  
    24  func NewSender(g *globals.Context) *Sender {
    25  	return &Sender{
    26  		Contextified: globals.NewContextified(g),
    27  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Wallet.Sender", false),
    28  	}
    29  }
    30  
    31  func (s *Sender) getConvParseInfo(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (parts []string, membersType chat1.ConversationMembersType, err error) {
    32  	conv, err := utils.GetUnverifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceAll)
    33  	if err != nil {
    34  		return parts, membersType, err
    35  	}
    36  	allParts, err := utils.GetConvParticipantUsernames(ctx, s.G(), uid, convID)
    37  	if err != nil {
    38  		return parts, membersType, err
    39  	}
    40  	switch conv.GetMembersType() {
    41  	case chat1.ConversationMembersType_TEAM:
    42  		return allParts, conv.GetMembersType(), nil
    43  	default:
    44  		nameParts := strings.Split(utils.GetRemoteConvTLFName(conv), ",")
    45  		nameMap := make(map[string]bool, len(nameParts))
    46  		for _, namePart := range nameParts {
    47  			nameMap[namePart] = true
    48  		}
    49  		for _, part := range allParts {
    50  			if nameMap[part] {
    51  				parts = append(parts, part)
    52  			}
    53  		}
    54  	}
    55  	return parts, conv.GetMembersType(), nil
    56  }
    57  
    58  func (s *Sender) getConvFullnames(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (res map[string]string, err error) {
    59  	uids, err := s.G().ParticipantsSource.Get(ctx, uid, convID, types.InboxSourceDataSourceAll)
    60  	if err != nil {
    61  		return res, err
    62  	}
    63  	kuids := make([]keybase1.UID, 0, len(uids))
    64  	for _, uid := range uids {
    65  		kuids = append(kuids, keybase1.UID(uid.String()))
    66  	}
    67  	rows, err := s.G().UIDMapper.MapUIDsToUsernamePackages(ctx, s.G(), kuids, time.Hour*24,
    68  		time.Minute, true)
    69  	if err != nil {
    70  		return res, err
    71  	}
    72  	res = make(map[string]string)
    73  	for _, row := range rows {
    74  		if row.FullName != nil {
    75  			res[row.NormalizedUsername.String()] = row.FullName.FullName.String()
    76  		}
    77  	}
    78  	return res, nil
    79  }
    80  
    81  func (s *Sender) getRecipientUsername(ctx context.Context, uid gregor1.UID, parts []string,
    82  	membersType chat1.ConversationMembersType, replyToUID gregor1.UID) (res string, err error) {
    83  	// If this message is a reply, infer the recipient as the original sender
    84  	if !(replyToUID.IsNil() || uid.Eq(replyToUID)) {
    85  		username, err := s.G().GetUPAKLoader().LookupUsername(ctx, keybase1.UID(replyToUID.String()))
    86  		if err != nil {
    87  			return res, err
    88  		}
    89  		return username.String(), nil
    90  	}
    91  
    92  	switch membersType {
    93  	case chat1.ConversationMembersType_TEAM:
    94  		return res, errors.New("must specify username in team chat")
    95  	default:
    96  	}
    97  	if len(parts) != 2 {
    98  		return res, fmt.Errorf("must specify username with more than two people: %d", len(parts))
    99  	}
   100  	username, err := s.G().GetUPAKLoader().LookupUsername(ctx, keybase1.UID(uid.String()))
   101  	if err != nil {
   102  		return res, err
   103  	}
   104  	if username.String() == parts[0] {
   105  		return parts[1], nil
   106  	}
   107  	return parts[0], nil
   108  }
   109  
   110  func (s *Sender) validConvUsername(ctx context.Context, username string, parts []string) bool {
   111  	for _, p := range parts {
   112  		if username == p {
   113  			return true
   114  		}
   115  	}
   116  	return false
   117  }
   118  
   119  func (s *Sender) ParsePayments(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   120  	body string, replyTo *chat1.MessageID) (res []types.ParsedStellarPayment) {
   121  	defer s.Trace(ctx, nil, "ParsePayments")()
   122  	parsed := FindChatTxCandidates(body)
   123  	if len(parsed) == 0 {
   124  		return nil
   125  	}
   126  
   127  	parts, membersType, err := s.getConvParseInfo(ctx, uid, convID)
   128  	if err != nil {
   129  		s.Debug(ctx, "ParsePayments: failed to getConvParseInfo %v", err)
   130  		return nil
   131  	}
   132  	replyToUID, err := s.handleReplyTo(ctx, uid, convID, replyTo)
   133  	if err != nil {
   134  		s.Debug(ctx, "ParsePayments: failed to handleReplyTo: %v", err)
   135  		return nil
   136  	}
   137  	seen := make(map[string]struct{})
   138  	for _, p := range parsed {
   139  		var username string
   140  		// The currency might be legit but `KnownCurrencyCodeInstant` may not have data yet.
   141  		// In that case (false, false) comes back and the entry is _not_ skipped.
   142  		if known, ok := s.G().GetStellar().KnownCurrencyCodeInstant(ctx, p.CurrencyCode); ok && !known {
   143  			continue
   144  		}
   145  		if p.Username == nil {
   146  			if username, err = s.getRecipientUsername(ctx, uid, parts, membersType, replyToUID); err != nil {
   147  				s.Debug(ctx, "ParsePayments: failed to get username, skipping: %s", err)
   148  				continue
   149  			}
   150  		} else if s.validConvUsername(ctx, *p.Username, parts) {
   151  			username = *p.Username
   152  		} else {
   153  			s.Debug(ctx, "ParsePayments: skipping mention for not being in conv")
   154  			continue
   155  		}
   156  		if _, ok := seen[p.Full]; ok {
   157  			continue
   158  		}
   159  		seen[p.Full] = struct{}{}
   160  		normalizedUn := libkb.NewNormalizedUsername(username)
   161  		if _, ok := seen[normalizedUn.String()]; ok {
   162  			continue
   163  		}
   164  		seen[normalizedUn.String()] = struct{}{}
   165  		res = append(res, types.ParsedStellarPayment{
   166  			Username: normalizedUn,
   167  			Amount:   p.Amount,
   168  			Currency: p.CurrencyCode,
   169  			Full:     p.Full,
   170  		})
   171  	}
   172  	return res
   173  }
   174  
   175  func (s *Sender) handleReplyTo(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, replyTo *chat1.MessageID) (gregor1.UID, error) {
   176  	if replyTo == nil {
   177  		return nil, nil
   178  	}
   179  	reply, err := s.G().ChatHelper.GetMessage(ctx, uid, convID, *replyTo, false, nil)
   180  	if err != nil {
   181  		s.Debug(ctx, "handleReplyTo: failed to get reply message: %s", err)
   182  		return nil, err
   183  	}
   184  	if !reply.IsValid() {
   185  		s.Debug(ctx, "handleReplyTo: reply message invalid: %v %v", replyTo, err)
   186  		return nil, nil
   187  	}
   188  	return reply.Valid().ClientHeader.Sender, nil
   189  }
   190  
   191  func (s *Sender) paymentsToMinis(payments []types.ParsedStellarPayment) (minis []libkb.MiniChatPayment) {
   192  	for _, p := range payments {
   193  		minis = append(minis, p.ToMini())
   194  	}
   195  	return minis
   196  }
   197  
   198  func (s *Sender) DescribePayments(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   199  	payments []types.ParsedStellarPayment) (res chat1.UIChatPaymentSummary, toSend []types.ParsedStellarPayment, err error) {
   200  	defer s.Trace(ctx, &err, "DescribePayments")()
   201  	specs, err := s.G().GetStellar().SpecMiniChatPayments(s.G().MetaContext(ctx), s.paymentsToMinis(payments))
   202  	if err != nil {
   203  		return res, toSend, err
   204  	}
   205  	fullnames, err := s.getConvFullnames(ctx, uid, convID)
   206  	if err != nil {
   207  		return res, toSend, err
   208  	}
   209  	res.XlmTotal = specs.XLMTotal
   210  	res.DisplayTotal = specs.DisplayTotal
   211  	for index, s := range specs.Specs {
   212  		var displayAmount *string
   213  		var errorMsg *string
   214  		if len(s.DisplayAmount) > 0 {
   215  			displayAmount = new(string)
   216  			*displayAmount = s.DisplayAmount
   217  		}
   218  		if s.Error != nil {
   219  			errorMsg = new(string)
   220  			*errorMsg = s.Error.Error()
   221  		} else {
   222  			toSend = append(toSend, payments[index])
   223  		}
   224  		res.Payments = append(res.Payments, chat1.UIChatPayment{
   225  			Username:      s.Username.String(),
   226  			FullName:      fullnames[s.Username.String()],
   227  			XlmAmount:     s.XLMAmount,
   228  			DisplayAmount: displayAmount,
   229  			Error:         errorMsg,
   230  		})
   231  	}
   232  	return res, toSend, nil
   233  }
   234  
   235  func (s *Sender) SendPayments(ctx context.Context, convID chat1.ConversationID, payments []types.ParsedStellarPayment) (res []chat1.TextPayment, err error) {
   236  	defer s.Trace(ctx, &err, "SendPayments")()
   237  	usernameToFull := make(map[string]string)
   238  	var minis []libkb.MiniChatPayment
   239  	for _, p := range payments {
   240  		minis = append(minis, p.ToMini())
   241  		usernameToFull[p.Username.String()] = p.Full
   242  	}
   243  	paymentRes, err := s.G().GetStellar().SendMiniChatPayments(s.G().MetaContext(ctx), convID, minis)
   244  	if err != nil {
   245  		return res, err
   246  	}
   247  	for _, p := range paymentRes {
   248  		tp := chat1.TextPayment{
   249  			Username:    p.Username.String(),
   250  			PaymentText: usernameToFull[p.Username.String()],
   251  		}
   252  		if p.Error != nil {
   253  			tp.Result = chat1.NewTextPaymentResultWithError(p.Error.Error())
   254  		} else {
   255  			tp.Result = chat1.NewTextPaymentResultWithSent(p.PaymentID)
   256  		}
   257  		res = append(res, tp)
   258  	}
   259  	return res, nil
   260  }
   261  
   262  func (s *Sender) DecorateWithPayments(ctx context.Context, body string, payments []chat1.TextPayment) string {
   263  	return DecorateWithPayments(ctx, body, payments)
   264  }