Skip to content

Commit 617ac80

Browse files
author
jeffyanta
authored
Improve Twitter registration (#107)
* Pull in unofficialy latest protos * Update Twitter store * Implement updated GetTwitterUser RPC * Fix Twitter worker with new protos and methods * Fix SaveUser in memory Twitter store for detecting duplicate tip addresses * Update Twitter store to keep track of used registration nonces * Make adjustments to Twitter worker with new registration flow * Add push when Twitter account is connected * Update used twitter nonce postgres table name * Pull in official latest protos * Remove addressed todo
1 parent 0a22e66 commit 617ac80

File tree

16 files changed

+502
-154
lines changed

16 files changed

+502
-154
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
firebase.google.com/go/v4 v4.8.0
77
github.com/aws/aws-sdk-go-v2 v0.17.0
88
github.com/bits-and-blooms/bloom/v3 v3.1.0
9-
github.com/code-payments/code-protobuf-api v1.14.0
9+
github.com/code-payments/code-protobuf-api v1.16.1
1010
github.com/emirpasic/gods v1.12.0
1111
github.com/envoyproxy/protoc-gen-validate v1.0.4
1212
github.com/golang-jwt/jwt/v5 v5.0.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
121121
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
122122
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
123123
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
124-
github.com/code-payments/code-protobuf-api v1.14.0 h1:HQOTZtIDGbEjWp7HDFD20Lpav4CbRhrM6GZrhxtfiZc=
125-
github.com/code-payments/code-protobuf-api v1.14.0/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU=
124+
github.com/code-payments/code-protobuf-api v1.16.1 h1:aQ5cwttkMR8nJmN2tTD+5mb+7aWuWgKhY2VpAAogplE=
125+
github.com/code-payments/code-protobuf-api v1.16.1/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU=
126126
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw=
127127
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
128128
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=

pkg/code/async/user/service.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,24 @@ import (
88

99
"github.com/code-payments/code-server/pkg/code/async"
1010
code_data "github.com/code-payments/code-server/pkg/code/data"
11+
push_lib "github.com/code-payments/code-server/pkg/push"
1112
"github.com/code-payments/code-server/pkg/sync"
1213
"github.com/code-payments/code-server/pkg/twitter"
1314
)
1415

1516
type service struct {
1617
log *logrus.Entry
1718
data code_data.Provider
19+
pusher push_lib.Provider
1820
twitterClient *twitter.Client
1921
userLocks *sync.StripedLock
2022
}
2123

22-
func New(twitterClient *twitter.Client, data code_data.Provider) async.Service {
24+
func New(data code_data.Provider, pusher push_lib.Provider, twitterClient *twitter.Client) async.Service {
2325
return &service{
2426
log: logrus.StandardLogger().WithField("service", "user"),
2527
data: data,
28+
pusher: pusher,
2629
twitterClient: twitterClient,
2730
userLocks: sync.NewStripedLock(1024),
2831
}

pkg/code/async/user/twitter.go

Lines changed: 150 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package async_user
22

33
import (
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

2327
const (
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

Comments
 (0)