@@ -2,9 +2,12 @@ package async_user
22
33import (
44 "context"
5+ "crypto/ed25519"
6+ "database/sql"
57 "strings"
68 "time"
79
10+ "github.com/google/uuid"
811 "github.com/mr-tron/base58"
912 "github.com/newrelic/go-agent/v3/newrelic"
1013 "github.com/pkg/errors"
@@ -15,13 +18,14 @@ import (
1518 "github.com/code-payments/code-server/pkg/code/common"
1619 "github.com/code-payments/code-server/pkg/code/data/account"
1720 "github.com/code-payments/code-server/pkg/code/data/twitter"
21+ push_util "github.com/code-payments/code-server/pkg/code/push"
1822 "github.com/code-payments/code-server/pkg/metrics"
1923 "github.com/code-payments/code-server/pkg/retry"
2024 twitter_lib "github.com/code-payments/code-server/pkg/twitter"
2125)
2226
2327const (
24- tipCardRegistrationPrefix = "CodeAccount: "
28+ tipCardRegistrationPrefix = "CodeAccount"
2529 maxTweetSearchResults = 100 // maximum allowed
2630)
2731
@@ -44,7 +48,7 @@ func (p *service) twitterRegistrationWorker(serviceCtx context.Context, interval
4448 defer m .End ()
4549 tracedCtx := newrelic .NewContext (serviceCtx , m )
4650
47- err = p .findNewTwitterRegistrations (tracedCtx )
51+ err = p .processNewTwitterRegistrations (tracedCtx )
4852 if err != nil {
4953 m .NoticeError (err )
5054 log .WithError (err ).Warn ("failure processing new twitter registrations" )
@@ -98,96 +102,56 @@ func (p *service) twitterUserInfoUpdateWorker(serviceCtx context.Context, interv
98102 return err
99103}
100104
101- func (p * service ) findNewTwitterRegistrations (ctx context.Context ) error {
102- var newlyProcessedTweets []string
103-
104- err := func () error {
105- var pageToken * string
106- processedUsernames := make (map [string ]any )
107- for {
108- tweets , nextPageToken , err := p .twitterClient .SearchRecentTweets (
109- ctx ,
110- tipCardRegistrationPrefix ,
111- maxTweetSearchResults ,
112- pageToken ,
113- )
114- if err != nil {
115- return errors .Wrap (err , "error searching tweets" )
116- }
117-
118- for _ , tweet := range tweets {
119- if tweet .AdditionalMetadata .Author == nil {
120- return errors .Errorf ("author missing in tweet %s" , tweet .ID )
121- }
122-
123- isTweetProcessed , err := p .data .IsTweetProcessed (ctx , tweet .ID )
124- if err != nil {
125- return errors .Wrap (err , "error checking if tweet is processed" )
126- } else if isTweetProcessed {
127- // Found a checkpoint, so stop processing
128- return nil
129- }
130-
131- // Oldest tweets go first, so we are guaranteed to checkpoint everything
132- newlyProcessedTweets = append ([]string {tweet .ID }, newlyProcessedTweets ... )
133-
134- // Avoid reprocessing a Twitter user and potentially overriding the
135- // tip address with something older.
136- if _ , ok := processedUsernames [tweet .AdditionalMetadata .Author .Username ]; ok {
137- continue
138- }
139-
140- // Attempt to find a tip account from the registration tweet
141- tipAccount , err := findTipAccountRegisteredInTweet (tweet )
142- switch err {
143- case nil :
144- case errTwitterInvalidRegistrationValue , errTwitterRegistrationNotFound :
145- continue
146- default :
147- return errors .Wrapf (err , "unexpected error processing tweet %s" , tweet .ID )
148- }
105+ func (p * service ) processNewTwitterRegistrations (ctx context.Context ) error {
106+ tweets , err := p .findNewRegistrationTweets (ctx )
107+ if err != nil {
108+ return errors .Wrap (err , "error finding new registration tweets" )
109+ }
149110
150- // Validate the new tip account
151- accountInfoRecord , err := p .data .GetAccountInfoByTokenAddress (ctx , tipAccount .PublicKey ().ToBase58 ())
152- switch err {
153- case nil :
154- // todo: potentially use a relationship account instead
155- if accountInfoRecord .AccountType != commonpb .AccountType_PRIMARY {
156- continue
157- }
158- case account .ErrAccountInfoNotFound :
159- continue
160- default :
161- return errors .Wrap (err , "error getting account info" )
162- }
111+ for _ , tweet := range tweets {
112+ if tweet .AdditionalMetadata .Author == nil {
113+ return errors .Errorf ("author missing in tweet %s" , tweet .ID )
114+ }
163115
164- processedUsernames [tweet .AdditionalMetadata .Author .Username ] = struct {}{}
116+ // Attempt to find a verified tip account from the registration tweet
117+ tipAccount , registrationNonce , err := p .findVerifiedTipAccountRegisteredInTweet (ctx , tweet )
118+ switch err {
119+ case nil :
120+ case errTwitterInvalidRegistrationValue , errTwitterRegistrationNotFound :
121+ continue
122+ default :
123+ return errors .Wrapf (err , "unexpected error processing tweet %s" , tweet .ID )
124+ }
165125
166- err = p .updateCachedTwitterUser (ctx , tweet .AdditionalMetadata .Author , tipAccount )
167- if err != nil {
168- return errors .Wrap (err , "error updating cached user state" )
169- }
126+ // Save the updated tipping information
127+ err = p .data .ExecuteInTx (ctx , sql .LevelDefault , func (ctx context.Context ) error {
128+ err = p .data .MarkTwitterNonceAsUsed (ctx , tweet .ID , * registrationNonce )
129+ if err != nil {
130+ return err
170131 }
171132
172- if nextPageToken == nil {
173- return nil
133+ err = p .updateCachedTwitterUser (ctx , tweet .AdditionalMetadata .Author , tipAccount )
134+ if err != nil {
135+ return err
174136 }
175- pageToken = nextPageToken
176- }
177- }()
178137
179- if err != nil {
180- return err
181- }
138+ err = p .data .MarkTweetAsProcessed (ctx , tweet .ID )
139+ if err != nil {
140+ return err
141+ }
182142
183- // Only update the processed tweet cache once we've found another checkpoint,
184- // or reached the end of the Tweet feed.
185- //
186- // todo: add batching
187- for _ , tweetId := range newlyProcessedTweets {
188- err := p .data .MarkTweetAsProcessed (ctx , tweetId )
189- if err != nil {
190- return errors .Wrap (err , "error marking tweet as processed" )
143+ return nil
144+ })
145+
146+ switch err {
147+ case nil :
148+ go push_util .SendTwitterAccountConnectedPushNotification (ctx , p .data , p .pusher , tipAccount )
149+ case twitter .ErrDuplicateTipAddress , twitter .ErrDuplicateNonce :
150+ // Any race conditions with duplicate nonces or tip addresses will are ignored
151+ //
152+ // todo: In the future, support multiple tip address mappings
153+ default :
154+ return errors .Wrap (err , "error saving new registration" )
191155 }
192156 }
193157
@@ -212,7 +176,7 @@ func (p *service) updateCachedTwitterUser(ctx context.Context, user *twitter_lib
212176 mu .Lock ()
213177 defer mu .Unlock ()
214178
215- record , err := p .data .GetTwitterUser (ctx , user .Username )
179+ record , err := p .data .GetTwitterUserByUsername (ctx , user .Username )
216180 switch err {
217181 case twitter .ErrUserNotFound :
218182 if newTipAccount == nil {
@@ -238,49 +202,129 @@ func (p *service) updateCachedTwitterUser(ctx context.Context, user *twitter_lib
238202 }
239203
240204 err = p .data .SaveTwitterUser (ctx , record )
241- if err != nil {
205+ switch err {
206+ case nil , twitter .ErrDuplicateTipAddress :
207+ return err
208+ default :
242209 return errors .Wrap (err , "error updating cached twitter user" )
243210 }
244- return nil
245211}
246212
247- func findTipAccountRegisteredInTweet (tweet * twitter_lib.Tweet ) (* common.Account , error ) {
248- var depositAccount * common.Account
213+ func (p * service ) findNewRegistrationTweets (ctx context.Context ) ([]* twitter_lib.Tweet , error ) {
214+ var pageToken * string
215+ var res []* twitter_lib.Tweet
216+ for {
217+ tweets , nextPageToken , err := p .twitterClient .SearchRecentTweets (
218+ ctx ,
219+ tipCardRegistrationPrefix ,
220+ maxTweetSearchResults ,
221+ pageToken ,
222+ )
223+ if err != nil {
224+ return nil , errors .Wrap (err , "error searching tweets" )
225+ }
226+
227+ for _ , tweet := range tweets {
228+ isTweetProcessed , err := p .data .IsTweetProcessed (ctx , tweet .ID )
229+ if err != nil {
230+ return nil , errors .Wrap (err , "error checking if tweet is processed" )
231+ } else if isTweetProcessed {
232+ // Found a checkpoint
233+ return res , nil
234+ }
249235
250- parts := strings .Fields (tweet .Text )
251- for _ , part := range parts {
252- if ! strings .HasPrefix (part , tipCardRegistrationPrefix ) {
236+ res = append ([]* twitter_lib.Tweet {tweet }, res ... )
237+ }
238+
239+ if nextPageToken == nil {
240+ return res , nil
241+ }
242+ pageToken = nextPageToken
243+ }
244+ }
245+
246+ func (p * service ) findVerifiedTipAccountRegisteredInTweet (ctx context.Context , tweet * twitter_lib.Tweet ) (* common.Account , * uuid.UUID , error ) {
247+ tweetParts := strings .Fields (tweet .Text )
248+ for _ , tweetPart := range tweetParts {
249+ // Look for the well-known prefix to indicate a potential registration value
250+
251+ if ! strings .HasPrefix (tweetPart , tipCardRegistrationPrefix ) {
253252 continue
254253 }
255254
256- part = part [len (tipCardRegistrationPrefix ):]
257- part = strings .TrimSuffix (part , "." )
255+ // Parse out the individual components of the registration value
256+
257+ tweetPart = strings .TrimSuffix (tweetPart , "." )
258+ registrationParts := strings .Split (tweetPart , ":" )
259+ if len (registrationParts ) != 4 {
260+ return nil , nil , errTwitterInvalidRegistrationValue
261+ }
262+
263+ addressString := registrationParts [1 ]
264+ nonceString := registrationParts [2 ]
265+ signatureString := registrationParts [3 ]
266+
267+ decodedAddress , err := base58 .Decode (addressString )
268+ if err != nil {
269+ return nil , nil , errTwitterInvalidRegistrationValue
270+ }
271+ if len (decodedAddress ) != 32 {
272+ return nil , nil , errTwitterInvalidRegistrationValue
273+ }
274+ tipAccount , _ := common .NewAccountFromPublicKeyBytes (decodedAddress )
275+
276+ nonce , err := uuid .Parse (nonceString )
277+ if err != nil {
278+ return nil , nil , errTwitterInvalidRegistrationValue
279+ }
258280
259- decoded , err := base58 .Decode (part )
281+ decodedSignature , err := base58 .Decode (signatureString )
260282 if err != nil {
261- return nil , errTwitterInvalidRegistrationValue
283+ return nil , nil , errTwitterInvalidRegistrationValue
284+ }
285+ if len (decodedSignature ) != 64 {
286+ return nil , nil , errTwitterInvalidRegistrationValue
287+ }
288+
289+ // Validate the components of the registration value
290+
291+ var tipAuthority * common.Account
292+ accountInfoRecord , err := p .data .GetAccountInfoByTokenAddress (ctx , tipAccount .PublicKey ().ToBase58 ())
293+ switch err {
294+ case nil :
295+ if accountInfoRecord .AccountType != commonpb .AccountType_PRIMARY {
296+ return nil , nil , errTwitterInvalidRegistrationValue
297+ }
298+
299+ tipAuthority , err = common .NewAccountFromPublicKeyString (accountInfoRecord .AuthorityAccount )
300+ if err != nil {
301+ return nil , nil , errors .Wrap (err , "invalid tip authority account" )
302+ }
303+ case account .ErrAccountInfoNotFound :
304+ return nil , nil , errTwitterInvalidRegistrationValue
305+ default :
306+ return nil , nil , errors .Wrap (err , "error getting account info" )
262307 }
263308
264- if len ( decoded ) != 32 {
265- return nil , errTwitterInvalidRegistrationValue
309+ if ! ed25519 . Verify ( tipAuthority . PublicKey (). ToBytes (), nonce [:], decodedSignature ) {
310+ return nil , nil , errTwitterInvalidRegistrationValue
266311 }
267312
268- depositAccount , _ = common .NewAccountFromPublicKeyBytes (decoded )
269- return depositAccount , nil
313+ return tipAccount , & nonce , nil
270314 }
271315
272- return nil , errTwitterRegistrationNotFound
316+ return nil , nil , errTwitterRegistrationNotFound
273317}
274318
275- func toProtoVerifiedType (value string ) userpb.GetTwitterUserResponse_VerifiedType {
319+ func toProtoVerifiedType (value string ) userpb.TwitterUser_VerifiedType {
276320 switch value {
277321 case "blue" :
278- return userpb .GetTwitterUserResponse_BLUE
322+ return userpb .TwitterUser_BLUE
279323 case "business" :
280- return userpb .GetTwitterUserResponse_BUSINESS
324+ return userpb .TwitterUser_BUSINESS
281325 case "government" :
282- return userpb .GetTwitterUserResponse_GOVERNMENT
326+ return userpb .TwitterUser_GOVERNMENT
283327 default :
284- return userpb .GetTwitterUserResponse_NONE
328+ return userpb .TwitterUser_NONE
285329 }
286330}
0 commit comments