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 }