Skip to content

Commit bf6a906

Browse files
authored
Enable clients to fetch Twitter user info (#95)
* Add Twitter user store * Implement the GetTwitterUser RPC
1 parent e1b4ee0 commit bf6a906

File tree

13 files changed

+658
-3
lines changed

13 files changed

+658
-3
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.13.0
9+
github.com/code-payments/code-protobuf-api v1.14.0
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.13.0 h1:FzRj1bYMJUgCH4ONM+lbOWrJDiPlBEaqwVlJklvo3bA=
125-
github.com/code-payments/code-protobuf-api v1.13.0/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU=
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=
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/data/internal.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import (
4747
"github.com/code-payments/code-server/pkg/code/data/timelock"
4848
"github.com/code-payments/code-server/pkg/code/data/transaction"
4949
"github.com/code-payments/code-server/pkg/code/data/treasury"
50+
"github.com/code-payments/code-server/pkg/code/data/twitter"
5051
"github.com/code-payments/code-server/pkg/code/data/user"
5152
"github.com/code-payments/code-server/pkg/code/data/user/identity"
5253
"github.com/code-payments/code-server/pkg/code/data/user/storage"
@@ -82,6 +83,7 @@ import (
8283
timelock_memory_client "github.com/code-payments/code-server/pkg/code/data/timelock/memory"
8384
transaction_memory_client "github.com/code-payments/code-server/pkg/code/data/transaction/memory"
8485
treasury_memory_client "github.com/code-payments/code-server/pkg/code/data/treasury/memory"
86+
twitter_memory_client "github.com/code-payments/code-server/pkg/code/data/twitter/memory"
8587
user_identity_memory_client "github.com/code-payments/code-server/pkg/code/data/user/identity/memory"
8688
user_storage_memory_client "github.com/code-payments/code-server/pkg/code/data/user/storage/memory"
8789
vault_memory_client "github.com/code-payments/code-server/pkg/code/data/vault/memory"
@@ -115,6 +117,7 @@ import (
115117
timelock_postgres_client "github.com/code-payments/code-server/pkg/code/data/timelock/postgres"
116118
transaction_postgres_client "github.com/code-payments/code-server/pkg/code/data/transaction/postgres"
117119
treasury_postgres_client "github.com/code-payments/code-server/pkg/code/data/treasury/postgres"
120+
twitter_postgres_client "github.com/code-payments/code-server/pkg/code/data/twitter/postgres"
118121
user_identity_postgres_client "github.com/code-payments/code-server/pkg/code/data/user/identity/postgres"
119122
user_storage_postgres_client "github.com/code-payments/code-server/pkg/code/data/user/storage/postgres"
120123
vault_postgres_client "github.com/code-payments/code-server/pkg/code/data/vault/postgres"
@@ -420,6 +423,11 @@ type DatabaseData interface {
420423
MarkIneligibleForAirdrop(ctx context.Context, owner string) error
421424
IsEligibleForAirdrop(ctx context.Context, owner string) (bool, error)
422425

426+
// Twitter
427+
// --------------------------------------------------------------------------------
428+
SaveTwitterUser(ctx context.Context, record *twitter.Record) error
429+
GetTwitterUser(ctx context.Context, username string) (*twitter.Record, error)
430+
423431
// ExecuteInTx executes fn with a single DB transaction that is scoped to the call.
424432
// This enables more complex transactions that can span many calls across the provider.
425433
//
@@ -462,6 +470,7 @@ type DatabaseProvider struct {
462470
onramp onramp.Store
463471
preferences preferences.Store
464472
airdrop airdrop.Store
473+
twitter twitter.Store
465474

466475
exchangeCache cache.Cache
467476
timelockCache cache.Cache
@@ -523,6 +532,7 @@ func NewDatabaseProvider(dbConfig *pg.Config) (DatabaseData, error) {
523532
onramp: onramp_postgres_client.New(db),
524533
preferences: preferences_postgres_client.New(db),
525534
airdrop: airdrop_postgres_client.New(db),
535+
twitter: twitter_postgres_client.New(db),
526536

527537
exchangeCache: cache.NewCache(maxExchangeRateCacheBudget),
528538
timelockCache: cache.NewCache(maxTimelockCacheBudget),
@@ -565,6 +575,7 @@ func NewTestDatabaseProvider() DatabaseData {
565575
onramp: onramp_memory_client.New(),
566576
preferences: preferences_memory_client.New(),
567577
airdrop: airdrop_memory_client.New(),
578+
twitter: twitter_memory_client.New(),
568579

569580
exchangeCache: cache.NewCache(maxExchangeRateCacheBudget),
570581
timelockCache: nil, // Shouldn't be used for tests
@@ -1499,3 +1510,12 @@ func (dp *DatabaseProvider) MarkIneligibleForAirdrop(ctx context.Context, owner
14991510
func (dp *DatabaseProvider) IsEligibleForAirdrop(ctx context.Context, owner string) (bool, error) {
15001511
return dp.airdrop.IsEligible(ctx, owner)
15011512
}
1513+
1514+
// Twitter
1515+
// --------------------------------------------------------------------------------
1516+
func (dp *DatabaseProvider) SaveTwitterUser(ctx context.Context, record *twitter.Record) error {
1517+
return dp.twitter.Save(ctx, record)
1518+
}
1519+
func (dp *DatabaseProvider) GetTwitterUser(ctx context.Context, username string) (*twitter.Record, error) {
1520+
return dp.twitter.Get(ctx, username)
1521+
}

pkg/code/data/twitter/memory/store.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package memory
2+
3+
import (
4+
"context"
5+
"sync"
6+
"time"
7+
8+
"github.com/code-payments/code-server/pkg/code/data/twitter"
9+
)
10+
11+
type store struct {
12+
mu sync.Mutex
13+
records []*twitter.Record
14+
last uint64
15+
}
16+
17+
// New returns a new in memory twitter.Store
18+
func New() twitter.Store {
19+
return &store{}
20+
}
21+
22+
// Put implements twitter.Store.Save
23+
func (s *store) Save(_ context.Context, data *twitter.Record) error {
24+
if err := data.Validate(); err != nil {
25+
return err
26+
}
27+
28+
s.mu.Lock()
29+
defer s.mu.Unlock()
30+
31+
s.last++
32+
if item := s.find(data); item != nil {
33+
data.LastUpdatedAt = time.Now()
34+
35+
item.Name = data.Name
36+
item.ProfilePicUrl = data.ProfilePicUrl
37+
item.VerifiedType = data.VerifiedType
38+
item.FollowerCount = data.FollowerCount
39+
item.TipAddress = data.TipAddress
40+
item.LastUpdatedAt = data.LastUpdatedAt
41+
} else {
42+
if data.Id == 0 {
43+
data.Id = s.last
44+
}
45+
if data.CreatedAt.IsZero() {
46+
data.CreatedAt = time.Now()
47+
}
48+
data.LastUpdatedAt = time.Now()
49+
50+
c := data.Clone()
51+
s.records = append(s.records, &c)
52+
}
53+
54+
return nil
55+
}
56+
57+
// Get implements twitter.Store.Get
58+
func (s *store) Get(_ context.Context, username string) (*twitter.Record, error) {
59+
s.mu.Lock()
60+
defer s.mu.Unlock()
61+
62+
item := s.findByUsername(username)
63+
if item == nil {
64+
return nil, twitter.ErrUserNotFound
65+
}
66+
67+
cloned := item.Clone()
68+
return &cloned, nil
69+
}
70+
71+
func (s *store) find(data *twitter.Record) *twitter.Record {
72+
for _, item := range s.records {
73+
if item.Id == data.Id {
74+
return item
75+
}
76+
if data.Username == item.Username {
77+
return item
78+
}
79+
}
80+
81+
return nil
82+
}
83+
84+
func (s *store) findByUsername(username string) *twitter.Record {
85+
for _, item := range s.records {
86+
if username == item.Username {
87+
return item
88+
}
89+
}
90+
91+
return nil
92+
}
93+
94+
func (s *store) reset() {
95+
s.mu.Lock()
96+
defer s.mu.Unlock()
97+
98+
s.records = nil
99+
s.last = 0
100+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package memory
2+
3+
import (
4+
"testing"
5+
6+
"github.com/code-payments/code-server/pkg/code/data/twitter/tests"
7+
)
8+
9+
func TestTwitterMemoryStore(t *testing.T) {
10+
testStore := New()
11+
teardown := func() {
12+
testStore.(*store).reset()
13+
}
14+
tests.RunTests(t, testStore, teardown)
15+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package postgres
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"time"
7+
8+
"github.com/jmoiron/sqlx"
9+
10+
"github.com/code-payments/code-protobuf-api/generated/go/user/v1"
11+
"github.com/code-payments/code-server/pkg/code/data/intent"
12+
"github.com/code-payments/code-server/pkg/code/data/twitter"
13+
pgutil "github.com/code-payments/code-server/pkg/database/postgres"
14+
)
15+
16+
const (
17+
tableName = "codewallet__core_twitteruser"
18+
)
19+
20+
type model struct {
21+
Id sql.NullInt64 `db:"id"`
22+
23+
Username string `db:"username"`
24+
Name string `db:"name"`
25+
ProfilePicUrl string `db:"profile_pic_url"`
26+
VerifiedType uint8 `db:"verified_type"`
27+
FollowerCount uint32 `db:"follower_count"`
28+
29+
TipAddress string `db:"tip_address"`
30+
31+
CreatedAt time.Time `db:"created_at"`
32+
LastUpdatedAt time.Time `db:"last_updated_at"`
33+
}
34+
35+
func toModel(r *twitter.Record) (*model, error) {
36+
if err := r.Validate(); err != nil {
37+
return nil, err
38+
}
39+
40+
return &model{
41+
Username: r.Username,
42+
Name: r.Name,
43+
ProfilePicUrl: r.ProfilePicUrl,
44+
VerifiedType: uint8(r.VerifiedType),
45+
FollowerCount: r.FollowerCount,
46+
47+
TipAddress: r.TipAddress,
48+
49+
CreatedAt: r.CreatedAt,
50+
LastUpdatedAt: r.LastUpdatedAt,
51+
}, nil
52+
}
53+
54+
func fromModel(m *model) *twitter.Record {
55+
return &twitter.Record{
56+
Id: uint64(m.Id.Int64),
57+
58+
Username: m.Username,
59+
Name: m.Name,
60+
ProfilePicUrl: m.ProfilePicUrl,
61+
VerifiedType: user.GetTwitterUserResponse_VerifiedType(m.VerifiedType),
62+
FollowerCount: m.FollowerCount,
63+
64+
TipAddress: m.TipAddress,
65+
66+
CreatedAt: m.CreatedAt,
67+
LastUpdatedAt: m.LastUpdatedAt,
68+
}
69+
}
70+
71+
func (m *model) dbSave(ctx context.Context, db *sqlx.DB) error {
72+
return pgutil.ExecuteInTx(ctx, db, sql.LevelDefault, func(tx *sqlx.Tx) error {
73+
query := `INSERT INTO ` + tableName + `
74+
(username, name, profile_pic_url, verified_type, follower_count, tip_address, created_at, last_updated_at)
75+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
76+
77+
ON CONFLICT (username)
78+
DO UPDATE
79+
SET name = $2, profile_pic_url = $3, verified_type = $4, follower_count = $5, tip_address = $6, created_at = $7, last_updated_at = $8
80+
WHERE ` + tableName + `.username = $1
81+
82+
RETURNING
83+
id, username, name, profile_pic_url, verified_type, follower_count, tip_address, created_at, last_updated_at`
84+
85+
if m.CreatedAt.IsZero() {
86+
m.CreatedAt = time.Now()
87+
}
88+
m.LastUpdatedAt = time.Now()
89+
90+
err := tx.QueryRowxContext(
91+
ctx,
92+
query,
93+
m.Username,
94+
m.Name,
95+
m.ProfilePicUrl,
96+
m.VerifiedType,
97+
m.FollowerCount,
98+
m.TipAddress,
99+
m.CreatedAt,
100+
m.LastUpdatedAt,
101+
).StructScan(m)
102+
103+
return pgutil.CheckNoRows(err, intent.ErrInvalidIntent)
104+
})
105+
}
106+
107+
func dbGet(ctx context.Context, db *sqlx.DB, username string) (*model, error) {
108+
res := &model{}
109+
110+
query := `SELECT
111+
id, username, name, profile_pic_url, verified_type, follower_count, tip_address, created_at, last_updated_at
112+
FROM ` + tableName + `
113+
WHERE username = $1
114+
LIMIT 1`
115+
116+
err := db.GetContext(ctx, res, query, username)
117+
if err != nil {
118+
return nil, pgutil.CheckNoRows(err, twitter.ErrUserNotFound)
119+
}
120+
return res, nil
121+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package postgres
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
7+
"github.com/code-payments/code-server/pkg/code/data/twitter"
8+
"github.com/jmoiron/sqlx"
9+
)
10+
11+
type store struct {
12+
db *sqlx.DB
13+
}
14+
15+
// New returns a new postgres twitter.Store
16+
func New(db *sql.DB) twitter.Store {
17+
return &store{
18+
db: sqlx.NewDb(db, "pgx"),
19+
}
20+
}
21+
22+
// Put implements twitter.Store.Save
23+
func (s *store) Save(ctx context.Context, record *twitter.Record) error {
24+
model, err := toModel(record)
25+
if err != nil {
26+
return err
27+
}
28+
29+
err = model.dbSave(ctx, s.db)
30+
if err != nil {
31+
return err
32+
}
33+
34+
res := fromModel(model)
35+
res.CopyTo(record)
36+
37+
return nil
38+
}
39+
40+
// Get implements twitter.Store.Get
41+
func (s *store) Get(ctx context.Context, username string) (*twitter.Record, error) {
42+
model, err := dbGet(ctx, s.db, username)
43+
if err != nil {
44+
return nil, err
45+
}
46+
return fromModel(model), nil
47+
}

0 commit comments

Comments
 (0)