diff --git a/api/geth_backend.go b/api/geth_backend.go index 0a1f0e0e1..6a19d9e3f 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -5,7 +5,6 @@ package api import ( "context" "database/sql" - "encoding/json" "errors" "fmt" "math/big" @@ -26,7 +25,6 @@ import ( "github.com/status-im/status-go/appdatabase" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" - "github.com/status-im/status-go/images" "github.com/status-im/status-go/logutils" "github.com/status-im/status-go/mailserver/registry" "github.com/status-im/status-go/multiaccounts" @@ -291,7 +289,7 @@ func (b *GethStatusBackend) startNodeWithKey(acc multiaccounts.Account, password return err } b.accountManager.SetAccountAddresses(walletAddr, watchAddrs...) - err = b.injectAccountIntoServices() + err = b.injectAccountsIntoServices() if err != nil { return err } @@ -595,7 +593,7 @@ func (b *GethStatusBackend) startNode(config *params.NodeConfig) (err error) { // Handle a case when a node is stopped and resumed. // If there is no account selected, an error is returned. if _, err := b.accountManager.SelectedChatAccount(); err == nil { - if err := b.injectAccountIntoServices(); err != nil { + if err := b.injectAccountsIntoServices(); err != nil { return err } } else if err != account.ErrNoAccountSelected { @@ -1099,14 +1097,14 @@ func (b *GethStatusBackend) SelectAccount(loginParams account.LoginParams) error return err } - if err := b.injectAccountIntoServices(); err != nil { + if err := b.injectAccountsIntoServices(); err != nil { return err } return nil } -func (b *GethStatusBackend) injectAccountIntoServices() error { +func (b *GethStatusBackend) injectAccountsIntoServices() error { chatAccount, err := b.accountManager.SelectedChatAccount() if err != nil { return err @@ -1135,7 +1133,7 @@ func (b *GethStatusBackend) injectAccountIntoServices() error { return err } - if err := st.InitProtocol(identity, b.appDB, logutils.ZapLogger()); err != nil { + if err := st.InitProtocol(identity, b.appDB, b.multiaccountsDB, logutils.ZapLogger()); err != nil { return err } return nil @@ -1163,7 +1161,7 @@ func (b *GethStatusBackend) injectAccountIntoServices() error { return err } - if err := st.InitProtocol(identity, b.appDB, logutils.ZapLogger()); err != nil { + if err := st.InitProtocol(identity, b.appDB, b.multiaccountsDB, logutils.ZapLogger()); err != nil { return err } } @@ -1225,7 +1223,7 @@ func (b *GethStatusBackend) InjectChatAccount(chatKeyHex, _ string) error { } b.accountManager.SetChatAccount(chatKey) - return b.injectAccountIntoServices() + return b.injectAccountsIntoServices() } func appendIf(condition bool, services []gethnode.ServiceConstructor, service gethnode.ServiceConstructor) []gethnode.ServiceConstructor { @@ -1270,56 +1268,3 @@ func (b *GethStatusBackend) SignHash(hexEncodedHash string) (string, error) { hexEncodedSignature := types.EncodeHex(signature) return hexEncodedSignature, nil } - -// GetIdentityImages returns an array of json marshalled IdentityImages assigned to the user's identity -func (b *GethStatusBackend) GetIdentityImages() (string, error) { - idb := images.NewDatabase(b.appDB) - iis, err := idb.GetIdentityImages() - if err != nil { - return "", err - } - - js, err := json.Marshal(iis) - - return string(js), err -} - -// GetIdentityImage returns a json object representing the image with the given name -func (b *GethStatusBackend) GetIdentityImage(name string) (string, error) { - idb := images.NewDatabase(b.appDB) - ii, err := idb.GetIdentityImage(name) - if err != nil { - return "", err - } - - js, err := json.Marshal(ii) - - return string(js), err -} - -// StoreIdentityImage takes the filepath of an image, crops it as per the rect coords and finally resizes the image. -// The resulting image(s) will be stored in the DB along with other user account information. -// aX and aY represent the pixel coordinates of the upper left corner of the image's cropping area -// bX and bY represent the pixel coordinates of the lower right corner of the image's cropping area -func (b *GethStatusBackend) StoreIdentityImage(filepath string, aX, aY, bX, bY int) (string, error) { - iis, err := images.GenerateIdentityImages(filepath, aX, aY, bX, bY) - if err != nil { - return "", err - } - - idb := images.NewDatabase(b.appDB) - err = idb.StoreIdentityImages(iis) - if err != nil { - return "", err - } - - js, err := json.Marshal(iis) - - return string(js), err -} - -// DeleteIdentityImage deletes an IdentityImage from the db with the given name -func (b *GethStatusBackend) DeleteIdentityImage(name string) error { - idb := images.NewDatabase(b.appDB) - return idb.DeleteIdentityImage(name) -} diff --git a/images/database.go b/images/database.go index 63b8f2fff..ad71bb26e 100644 --- a/images/database.go +++ b/images/database.go @@ -1,17 +1,12 @@ package images import ( - "context" - "database/sql" "encoding/json" "errors" ) -type Database struct { - db *sql.DB -} - type IdentityImage struct { + KeyUID string Name string Payload []byte Width int @@ -20,89 +15,6 @@ type IdentityImage struct { ResizeTarget int } -func NewDatabase(db *sql.DB) Database { - return Database{db: db} -} - -func (d *Database) GetIdentityImages() ([]*IdentityImage, error) { - rows, err := d.db.Query(`SELECT name, image_payload, width, height, file_size, resize_target FROM identity_images`) - if err != nil { - return nil, err - } - defer rows.Close() - - var iis []*IdentityImage - for rows.Next() { - ii := &IdentityImage{} - err = rows.Scan(&ii.Name, &ii.Payload, &ii.Width, &ii.Height, &ii.FileSize, &ii.ResizeTarget) - if err != nil { - return nil, err - } - - iis = append(iis, ii) - } - - return iis, nil -} - -func (d *Database) GetIdentityImage(it string) (*IdentityImage, error) { - query := "SELECT name, image_payload, width, height, file_size, resize_target FROM identity_images WHERE name = ?" - rows, err := d.db.Query(query, it) - if err != nil { - return nil, err - } - defer rows.Close() - - var ii IdentityImage - for rows.Next() { - err = rows.Scan(&ii.Name, &ii.Payload, &ii.Width, &ii.Height, &ii.FileSize, &ii.ResizeTarget) - if err != nil { - return nil, err - } - } - - return &ii, nil -} - -func (d *Database) StoreIdentityImages(iis []*IdentityImage) (err error) { - tx, err := d.db.BeginTx(context.Background(), &sql.TxOptions{}) - if err != nil { - return - } - defer func() { - if err == nil { - err = tx.Commit() - return - } - // don't shadow original error - _ = tx.Rollback() - }() - - query := "INSERT INTO identity_images (name, image_payload, width, height, file_size, resize_target) VALUES (?, ?, ?, ?, ?, ?)" - stmt, err := tx.Prepare(query) - if err != nil { - return - } - defer stmt.Close() - - for _, ii := range iis { - if ii == nil { - continue - } - _, err = stmt.Exec(ii.Name, ii.Payload, ii.Width, ii.Height, ii.FileSize, ii.ResizeTarget) - if err != nil { - return - } - } - - return -} - -func (d *Database) DeleteIdentityImage(it string) error { - _, err := d.db.Exec(`DELETE FROM identity_images WHERE name = ?`, it) - return err -} - func (i IdentityImage) GetType() (ImageType, error) { it := GetType(i.Payload) if it == UNKNOWN { @@ -123,6 +35,7 @@ func (i IdentityImage) MarshalJSON() ([]byte, error) { } temp := struct { + KeyUID string `json:"key_uid"` Name string `json:"type"` URI string `json:"uri"` Width int `json:"width"` @@ -130,6 +43,7 @@ func (i IdentityImage) MarshalJSON() ([]byte, error) { FileSize int `json:"file_size"` ResizeTarget int `json:"resize_target"` }{ + KeyUID: i.KeyUID, Name: i.Name, URI: uri, Width: i.Width, diff --git a/images/database_test.go b/images/database_test.go index 22b4b0070..2d8ea1eb8 100644 --- a/images/database_test.go +++ b/images/database_test.go @@ -3,12 +3,8 @@ package images import ( "encoding/json" "errors" - "io/ioutil" - "os" "testing" - "github.com/status-im/status-go/appdatabase" - "github.com/stretchr/testify/require" ) @@ -73,93 +69,3 @@ func TestIdentityImage_MarshalJSON(t *testing.T) { require.NoError(t, err) require.Exactly(t, expected, string(js)) } - -func setupTestDB(t *testing.T) (Database, func()) { - tmpfile, err := ioutil.TempFile("", "images-tests-") - require.NoError(t, err) - db, err := appdatabase.InitializeDB(tmpfile.Name(), "images-tests") - require.NoError(t, err) - return NewDatabase(db), func() { - require.NoError(t, db.Close()) - require.NoError(t, os.Remove(tmpfile.Name())) - } -} - -func seedTestDB(t *testing.T, db Database) { - iis := []*IdentityImage{ - { - Name: SmallDimName, - Payload: testJpegBytes, - Width: 80, - Height: 80, - FileSize: 256, - ResizeTarget: 80, - }, - { - Name: LargeDimName, - Payload: testPngBytes, - Width: 240, - Height: 300, - FileSize: 1024, - ResizeTarget: 240, - }, - } - - require.NoError(t, db.StoreIdentityImages(iis)) -} - -func TestDatabase_GetIdentityImages(t *testing.T) { - db, stop := setupTestDB(t) - defer stop() - seedTestDB(t, db) - - expected := `[{"type":"large","uri":"data:image/png;base64,iVBORw0KGgoAAAANSUg=","width":240,"height":300,"file_size":1024,"resize_target":240},{"type":"thumbnail","uri":"data:image/jpeg;base64,/9j/2wCEAFA3PEY8MlA=","width":80,"height":80,"file_size":256,"resize_target":80}]` - - oiis, err := db.GetIdentityImages() - require.NoError(t, err) - - joiis, err := json.Marshal(oiis) - require.NoError(t, err) - require.Exactly(t, expected, string(joiis)) -} - -func TestDatabase_GetIdentityImage(t *testing.T) { - db, stop := setupTestDB(t) - defer stop() - seedTestDB(t, db) - - cs := []struct { - Name string - Expected string - }{ - { - SmallDimName, - `{"type":"thumbnail","uri":"data:image/jpeg;base64,/9j/2wCEAFA3PEY8MlA=","width":80,"height":80,"file_size":256,"resize_target":80}`, - }, - { - LargeDimName, - `{"type":"large","uri":"data:image/png;base64,iVBORw0KGgoAAAANSUg=","width":240,"height":300,"file_size":1024,"resize_target":240}`, - }, - } - - for _, c := range cs { - oii, err := db.GetIdentityImage(c.Name) - require.NoError(t, err) - - joii, err := json.Marshal(oii) - require.NoError(t, err) - require.Exactly(t, c.Expected, string(joii)) - } -} - -func TestDatabase_DeleteIdentityImage(t *testing.T) { - db, stop := setupTestDB(t) - defer stop() - seedTestDB(t, db) - - require.NoError(t, db.DeleteIdentityImage(SmallDimName)) - - oii, err := db.GetIdentityImage(SmallDimName) - require.NoError(t, err) - require.Empty(t, oii) -} diff --git a/images/decode_test.go b/images/decode_test.go index 346053ca4..13a9e263e 100644 --- a/images/decode_test.go +++ b/images/decode_test.go @@ -12,14 +12,6 @@ const ( path = "../_assets/tests/" ) -var ( - testJpegBytes = []byte{0xff, 0xd8, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x50} - testPngBytes = []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48} - testGifBytes = []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x00, 0x01, 0x00, 0x01, 0x84, 0x1f, 0x00, 0xff} - testWebpBytes = []byte{0x52, 0x49, 0x46, 0x46, 0x90, 0x49, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50} - testAacBytes = []byte{0xff, 0xf1, 0x50, 0x80, 0x1c, 0x3f, 0xfc, 0xda, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35} -) - func TestDecode(t *testing.T) { cs := []struct { diff --git a/images/encode.go b/images/encode.go index 5422b8ec3..637e0637c 100644 --- a/images/encode.go +++ b/images/encode.go @@ -58,6 +58,10 @@ func EncodeToBestSize(bb *bytes.Buffer, img image.Image, size ResizeDimension) e } func GetPayloadDataURI(payload []byte) (string, error) { + if len(payload) == 0 { + return "", nil + } + mt, err := GetMimeType(payload) if err != nil { return "", err diff --git a/images/test_data.go b/images/test_data.go new file mode 100644 index 000000000..a8c0d38ac --- /dev/null +++ b/images/test_data.go @@ -0,0 +1,31 @@ +package images + +// Test data that would typically only exist in a test file, used for exporting sample data outside the package. +var ( + testJpegBytes = []byte{0xff, 0xd8, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x50, 0x37, 0x3c, 0x46, 0x3c, 0x32, 0x50} + testPngBytes = []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48} + testGifBytes = []byte{0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x00, 0x01, 0x00, 0x01, 0x84, 0x1f, 0x00, 0xff} + testWebpBytes = []byte{0x52, 0x49, 0x46, 0x46, 0x90, 0x49, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50} + testAacBytes = []byte{0xff, 0xf1, 0x50, 0x80, 0x1c, 0x3f, 0xfc, 0xda, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35} +) + +func SampleIdentityImages() []*IdentityImage { + return []*IdentityImage{ + { + Name: SmallDimName, + Payload: testJpegBytes, + Width: 80, + Height: 80, + FileSize: 256, + ResizeTarget: 80, + }, + { + Name: LargeDimName, + Payload: testPngBytes, + Width: 240, + Height: 300, + FileSize: 1024, + ResizeTarget: 240, + }, + } +} diff --git a/mobile/status.go b/mobile/status.go index 372702d9d..69f865a67 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -628,46 +628,3 @@ func MultiformatDeserializePublicKey(key, outBase string) string { return pk } - -// GetProfileImages returns an array of json marshalled IdentityImages assigned to the user's identity -func GetProfileImages() string { - pis, err := statusBackend.GetIdentityImages() - if err != nil { - return makeJSONResponse(err) - } - - return pis -} - -// GetProfileImage returns a json object representing the image with the given name -func GetProfileImage(name string) string { - ii, err := statusBackend.GetIdentityImage(name) - if err != nil { - return makeJSONResponse(err) - } - - return ii -} - -// SaveProfileImage takes the filepath of an image, crops it as per the rect coords and finally resizes the image. -// The resulting image(s) will be stored in the DB along with other user account information. -// aX and aY represent the pixel coordinates of the upper left corner of the image's cropping area -// bX and bY represent the pixel coordinates of the lower right corner of the image's cropping area -func SaveProfileImage(filepath string, aX, aY, bX, bY int) string { - imgs, err := statusBackend.StoreIdentityImage(filepath, aX, aY, bX, bY) - if err != nil { - return makeJSONResponse(err) - } - - return imgs -} - -// DeleteProfileImage deletes an IdentityImage from the db with the given name -func DeleteProfileImage(name string) string { - err := statusBackend.DeleteIdentityImage(name) - if err != nil { - return makeJSONResponse(err) - } - - return "ok" -} diff --git a/multiaccounts/accounts/database.go b/multiaccounts/accounts/database.go index 6b1cecf16..519e28246 100644 --- a/multiaccounts/accounts/database.go +++ b/multiaccounts/accounts/database.go @@ -122,6 +122,7 @@ func (db Database) Close() error { return db.db.Close() } +// TODO remove photoPath from settings func (db *Database) CreateSettings(s Settings, nodecfg params.NodeConfig) error { _, err := db.db.Exec(` INSERT INTO settings ( diff --git a/multiaccounts/database.go b/multiaccounts/database.go index ef41426a2..eaf670791 100644 --- a/multiaccounts/database.go +++ b/multiaccounts/database.go @@ -1,19 +1,28 @@ package multiaccounts import ( + "context" "database/sql" + "github.com/status-im/status-go/images" "github.com/status-im/status-go/multiaccounts/migrations" "github.com/status-im/status-go/sqlite" ) +// TODO remove reference to PhotoPath +// TODO write migration to drop PhotoPath + // Account stores public information about account. type Account struct { - Name string `json:"name"` - Timestamp int64 `json:"timestamp"` - PhotoPath string `json:"photo-path"` - KeycardPairing string `json:"keycard-pairing"` - KeyUID string `json:"key-uid"` + Name string `json:"name"` + Timestamp int64 `json:"timestamp"` + KeycardPairing string `json:"keycard-pairing"` + KeyUID string `json:"key-uid"` + ImageURIs []string `json:"image-uris"` +} + +type Database struct { + db *sql.DB } // InitializeDB creates db file at a given path and applies migrations. @@ -29,25 +38,21 @@ func InitializeDB(path string) (*Database, error) { return &Database{db: db}, nil } -type Database struct { - db *sql.DB -} - func (db *Database) Close() error { return db.db.Close() } func (db *Database) GetAccounts() ([]Account, error) { - rows, err := db.db.Query("SELECT name, loginTimestamp, photoPath, keycardPairing, keyUid from accounts ORDER BY loginTimestamp DESC") + rows, err := db.db.Query("SELECT name, loginTimestamp, keycardPairing, keyUid from accounts ORDER BY loginTimestamp DESC") if err != nil { return nil, err } defer rows.Close() - rst := []Account{} + var rst []Account inthelper := sql.NullInt64{} for rows.Next() { acc := Account{} - err = rows.Scan(&acc.Name, &inthelper, &acc.PhotoPath, &acc.KeycardPairing, &acc.KeyUID) + err = rows.Scan(&acc.Name, &inthelper, &acc.KeycardPairing, &acc.KeyUID) if err != nil { return nil, err } @@ -58,12 +63,12 @@ func (db *Database) GetAccounts() ([]Account, error) { } func (db *Database) SaveAccount(account Account) error { - _, err := db.db.Exec("INSERT OR REPLACE INTO accounts (name, photoPath, keycardPairing, keyUid) VALUES (?, ?, ?, ?)", account.Name, account.PhotoPath, account.KeycardPairing, account.KeyUID) + _, err := db.db.Exec("INSERT OR REPLACE INTO accounts (name, keycardPairing, keyUid) VALUES (?, ?, ?)", account.Name, account.KeycardPairing, account.KeyUID) return err } func (db *Database) UpdateAccount(account Account) error { - _, err := db.db.Exec("UPDATE accounts SET name = ?, photoPath = ?, keycardPairing = ? WHERE keyUid = ?", account.Name, account.PhotoPath, account.KeycardPairing, account.KeyUID) + _, err := db.db.Exec("UPDATE accounts SET name = ?, keycardPairing = ? WHERE keyUid = ?", account.Name, account.KeycardPairing, account.KeyUID) return err } @@ -76,3 +81,87 @@ func (db *Database) DeleteAccount(keyUID string) error { _, err := db.db.Exec("DELETE FROM accounts WHERE keyUid = ?", keyUID) return err } + +// Account images + +func (db *Database) GetIdentityImages(keyUid string) ([]*images.IdentityImage, error) { + rows, err := db.db.Query(`SELECT key_uid, name, image_payload, width, height, file_size, resize_target FROM identity_images WHERE key_uid = ?`, keyUid) + if err != nil { + return nil, err + } + defer rows.Close() + + var iis []*images.IdentityImage + for rows.Next() { + ii := &images.IdentityImage{} + err = rows.Scan(&ii.KeyUID, &ii.Name, &ii.Payload, &ii.Width, &ii.Height, &ii.FileSize, &ii.ResizeTarget) + if err != nil { + return nil, err + } + + iis = append(iis, ii) + } + + return iis, nil +} + +func (db *Database) GetIdentityImage(keyUid, it string) (*images.IdentityImage, error) { + rows, err := db.db.Query("SELECT key_uid, name, image_payload, width, height, file_size, resize_target FROM identity_images WHERE key_uid = ? AND name = ?", keyUid, it) + if err != nil { + return nil, err + } + defer rows.Close() + + var ii images.IdentityImage + for rows.Next() { + err = rows.Scan(&ii.KeyUID ,&ii.Name, &ii.Payload, &ii.Width, &ii.Height, &ii.FileSize, &ii.ResizeTarget) + if err != nil { + return nil, err + } + } + + return &ii, nil +} + +func (db *Database) StoreIdentityImages(keyUid string, iis []*images.IdentityImage) error { + // Because SQL INSERTs are triggered in a loop use a tx to ensure a single call to the DB. + tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{}) + if err != nil { + return err + } + defer func() { + if err == nil { + err = tx.Commit() + return + } + // don't shadow original error + _ = tx.Rollback() + }() + + for _, ii := range iis { + if ii == nil { + continue + } + + _, err := tx.Exec( + "INSERT INTO identity_images (key_uid, name, image_payload, width, height, file_size, resize_target) VALUES (?, ?, ?, ?, ?, ?, ?)", + keyUid, + ii.Name, + ii.Payload, + ii.Width, + ii.Height, + ii.FileSize, + ii.ResizeTarget, + ) + if err != nil { + return err + } + } + + return nil +} + +func (db *Database) DeleteIdentityImage(keyUid string) error { + _, err := db.db.Exec(`DELETE FROM identity_images WHERE key_uid = ?`, keyUid) + return err +} diff --git a/multiaccounts/database_test.go b/multiaccounts/database_test.go index b08206e3c..7a94d9e57 100644 --- a/multiaccounts/database_test.go +++ b/multiaccounts/database_test.go @@ -1,10 +1,13 @@ package multiaccounts import ( + "encoding/json" "io/ioutil" "os" "testing" + "github.com/status-im/status-go/images" + "github.com/stretchr/testify/require" ) @@ -35,7 +38,7 @@ func TestAccountsUpdate(t *testing.T) { defer stop() expected := Account{KeyUID: "string"} require.NoError(t, db.SaveAccount(expected)) - expected.PhotoPath = "chars" + expected.Name = "chars" require.NoError(t, db.UpdateAccount(expected)) rst, err := db.GetAccounts() require.NoError(t, err) @@ -59,3 +62,84 @@ func TestLoginUpdate(t *testing.T) { require.NoError(t, err) require.Equal(t, accounts, rst) } + +// Profile Image tests + +var ( + keyUid = "0xdeadbeef" + keyUid2 = "0x1337beef" +) + +func seedTestDB(t *testing.T, db *Database) { + iis := images.SampleIdentityImages() + require.NoError(t, db.StoreIdentityImages(keyUid, iis)) +} + +func TestDatabase_GetIdentityImages(t *testing.T) { + db, stop := setupTestDB(t) + defer stop() + seedTestDB(t, db) + + expected := `[{"key_uid":"0xdeadbeef","type":"large","uri":"data:image/png;base64,iVBORw0KGgoAAAANSUg=","width":240,"height":300,"file_size":1024,"resize_target":240},{"key_uid":"0xdeadbeef","type":"thumbnail","uri":"data:image/jpeg;base64,/9j/2wCEAFA3PEY8MlA=","width":80,"height":80,"file_size":256,"resize_target":80}]` + + oiis, err := db.GetIdentityImages(keyUid) + require.NoError(t, err) + + joiis, err := json.Marshal(oiis) + require.NoError(t, err) + require.Exactly(t, expected, string(joiis)) + + oiis, err = db.GetIdentityImages(keyUid2) + require.NoError(t, err) + + require.Exactly(t, 0, len(oiis)) +} + +func TestDatabase_GetIdentityImage(t *testing.T) { + db, stop := setupTestDB(t) + defer stop() + seedTestDB(t, db) + + cs := []struct { + KeyUid string + Name string + Expected string + }{ + { + keyUid, + images.SmallDimName, + `{"key_uid":"0xdeadbeef","type":"thumbnail","uri":"data:image/jpeg;base64,/9j/2wCEAFA3PEY8MlA=","width":80,"height":80,"file_size":256,"resize_target":80}`, + }, + { + keyUid, + images.LargeDimName, + `{"key_uid":"0xdeadbeef","type":"large","uri":"data:image/png;base64,iVBORw0KGgoAAAANSUg=","width":240,"height":300,"file_size":1024,"resize_target":240}`, + }, + { + keyUid2, + images.LargeDimName, + `{"key_uid":"","type":"","uri":"","width":0,"height":0,"file_size":0,"resize_target":0}`, + }, + } + + for _, c := range cs { + oii, err := db.GetIdentityImage(c.KeyUid, c.Name) + require.NoError(t, err) + + joii, err := json.Marshal(oii) + require.NoError(t, err) + require.Exactly(t, c.Expected, string(joii)) + } +} + +func TestDatabase_DeleteIdentityImage(t *testing.T) { + db, stop := setupTestDB(t) + defer stop() + seedTestDB(t, db) + + require.NoError(t, db.DeleteIdentityImage(keyUid)) + + oii, err := db.GetIdentityImage(keyUid, images.SmallDimName) + require.NoError(t, err) + require.Empty(t, oii) +} diff --git a/multiaccounts/migrations/bindata.go b/multiaccounts/migrations/bindata.go index 098ac68cb..bdd61f307 100644 --- a/multiaccounts/migrations/bindata.go +++ b/multiaccounts/migrations/bindata.go @@ -3,7 +3,7 @@ // 0001_accounts.down.sql (21B) // 0001_accounts.up.sql (163B) // 1605007189_identity_images.down.sql (29B) -// 1605007189_identity_images.up.sql (266B) +// 1605007189_identity_images.up.sql (268B) // doc.go (74B) package migrations @@ -133,7 +133,7 @@ func _1605007189_identity_imagesDownSql() (*asset, error) { return a, nil } -var __1605007189_identity_imagesUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\xce\xc1\x6a\xc3\x30\x10\x04\xd0\xbb\xc1\xff\x30\xc7\x04\xf2\x07\x3d\xc9\xaa\x42\x44\x55\x29\x28\x72\xd3\x9c\x84\x40\x5b\x7b\x69\xe2\x96\x44\x50\xdc\xaf\x2f\x75\x7c\x30\x39\xee\x63\x76\x18\xe9\x95\x08\x0a\x41\x34\x46\x41\x6f\x61\x5d\x80\x7a\xd7\x87\x70\x00\x67\x1a\x0a\x97\x31\xf2\x25\x75\x74\x5b\xd5\x15\x00\x7c\xd2\xd8\x72\xc6\x9b\xf0\x72\x27\xfc\xe6\x8e\x43\xba\xd0\x03\x4d\x4f\xf1\x3b\x8d\xe7\xaf\x94\xd1\x18\xd7\x4c\xdd\xb6\x35\x66\x4e\xfc\x70\x2e\x3d\x78\x28\xf3\xdd\x13\x77\x7d\x59\xc0\x07\x9f\x29\xde\xf8\x97\x16\x76\xa5\x7f\x88\x25\x5d\x3b\x5a\x66\xf7\x5e\xbf\x0a\x7f\xc2\x8b\x3a\x61\x75\xdf\xb8\x99\x66\xad\xe1\x2c\xa4\xb3\x5b\xa3\x65\x80\x57\x7b\x23\xa4\xaa\xab\x35\x8e\x3a\xec\x5c\x1b\xe0\xdd\x51\x3f\x3f\xd5\xd5\x5f\x00\x00\x00\xff\xff\xec\x31\x60\x93\x0a\x01\x00\x00") +var __1605007189_identity_imagesUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\xce\xc1\x6a\xc3\x30\x10\x04\xd0\xbb\xc1\xff\x30\xc7\x04\xf2\x07\x3d\xc9\xaa\x42\x44\x55\x29\x28\x4a\xd3\x9c\x84\x40\x5b\x7b\x69\xe2\x96\x58\xa5\xb8\x5f\x5f\xea\xfa\x60\x72\xdc\xc7\xec\x30\xd2\x2b\x11\x14\x82\x68\x8c\x82\xde\xc2\xba\x00\xf5\xaa\x0f\xe1\x00\xce\xd4\x17\x2e\x63\xe4\x6b\x6a\x69\x58\xd5\x15\x00\xbc\xd3\x18\xbf\x38\xe3\x45\x78\xb9\x13\x7e\xf3\xaf\x7d\xba\xd2\x1d\x4d\x5f\xf1\x33\x8d\x97\x8f\x94\xd1\x18\xd7\x4c\xe5\xf6\x68\xcc\x9c\xf8\xe6\x5c\x3a\x70\x5f\xe6\xbb\x23\x6e\xbb\xb2\x80\x37\xbe\x50\x1c\xf8\x87\x16\x76\xa3\x3f\x88\x25\xdd\x5a\x5a\x66\xf7\x5e\x3f\x0b\x7f\xc6\x93\x3a\x63\x35\x8f\xdc\x4c\xbb\xd6\x70\x16\xd2\xd9\xad\xd1\x32\xc0\xab\xbd\x11\x52\xd5\xd5\x1a\x27\x1d\x76\xee\x18\xe0\xdd\x49\x3f\x3e\xd4\xd5\x6f\x00\x00\x00\xff\xff\x8c\x6a\x0a\x57\x0c\x01\x00\x00") func _1605007189_identity_imagesUpSqlBytes() ([]byte, error) { return bindataRead( @@ -148,8 +148,8 @@ func _1605007189_identity_imagesUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1605007189_identity_images.up.sql", size: 266, mode: os.FileMode(0644), modTime: time.Unix(1608048507, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe, 0x33, 0xd, 0xf1, 0x5c, 0xb0, 0x64, 0x4e, 0xec, 0x89, 0xa1, 0x6, 0x56, 0x70, 0x36, 0xca, 0xe5, 0x6c, 0xd8, 0xdd, 0x45, 0xce, 0xc3, 0xe9, 0x66, 0xbd, 0x1c, 0x23, 0xe8, 0x42, 0xb6, 0x17}} + info := bindataFileInfo{name: "1605007189_identity_images.up.sql", size: 268, mode: os.FileMode(0644), modTime: time.Unix(1608048533, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x50, 0xb6, 0xc1, 0x5c, 0x76, 0x72, 0x6b, 0x22, 0x34, 0xdc, 0x96, 0xdc, 0x2b, 0xfd, 0x2d, 0xbe, 0xcc, 0x1e, 0xd4, 0x5, 0x93, 0xd, 0xc2, 0x51, 0xf3, 0x1a, 0xef, 0x2b, 0x26, 0xa4, 0xeb, 0x65}} return a, nil } diff --git a/multiaccounts/migrations/sql/1605007189_identity_images.up.sql b/multiaccounts/migrations/sql/1605007189_identity_images.up.sql index 53782ff18..126379dbc 100644 --- a/multiaccounts/migrations/sql/1605007189_identity_images.up.sql +++ b/multiaccounts/migrations/sql/1605007189_identity_images.up.sql @@ -1,10 +1,10 @@ CREATE TABLE IF NOT EXISTS identity_images( - keyUid VARCHAR, + key_uid VARCHAR, name VARCHAR, image_payload BLOB NOT NULL, width int, height int, file_size int, resize_target int, - PRIMARY KEY (keyUid, name) ON CONFLICT REPLACE + PRIMARY KEY (key_uid, name) ON CONFLICT REPLACE ) WITHOUT ROWID; diff --git a/protocol/messenger.go b/protocol/messenger.go index 6b3ca062b..d1d9c74fd 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -3,8 +3,11 @@ package protocol import ( "context" "crypto/ecdsa" + "crypto/sha256" "database/sql" "encoding/hex" + gethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/status-im/status-go/multiaccounts" "io/ioutil" "math" "math/rand" @@ -78,6 +81,7 @@ type Messenger struct { installationID string mailserver []byte database *sql.DB + multiAccounts *multiaccounts.Database quit chan struct{} mutex sync.Mutex @@ -277,6 +281,7 @@ func NewMessenger( messagesPersistenceEnabled: c.messagesPersistenceEnabled, verifyTransactionClient: c.verifyTransactionClient, database: database, + multiAccounts: c.multiAccount, quit: make(chan struct{}), shutdownTasks: []func() error{ database.Close, @@ -565,6 +570,9 @@ func (m *Messenger) shouldPublishChatIdentity(chatId string) (bool, error) { // context 'public-chat' will attach only the 'thumbnail' IdentityImage // context 'private-chat' will attach all IdentityImage func (m *Messenger) createChatIdentity(context string) (*protobuf.ChatIdentity, error) { + keyUIDBytes := sha256.Sum256(gethcrypto.FromECDSAPub(&m.identity.PublicKey)) + keyUID := types.EncodeHex(keyUIDBytes[:]) + ci := &protobuf.ChatIdentity{ Clock: m.transport.GetCurrentTime(), EnsName: "", // TODO add ENS name handling to dedicate PR @@ -574,8 +582,7 @@ func (m *Messenger) createChatIdentity(context string) (*protobuf.ChatIdentity, switch context { case "public-chat": - idb := userimage.NewDatabase(m.database) - img, err := idb.GetIdentityImage(userimage.SmallDimName) + img, err := m.multiAccounts.GetIdentityImage(keyUID, userimage.SmallDimName) if err != nil { return nil, err } @@ -584,8 +591,7 @@ func (m *Messenger) createChatIdentity(context string) (*protobuf.ChatIdentity, ci.Images = ciis case "private-chat": - idb := userimage.NewDatabase(m.database) - imgs, err := idb.GetIdentityImages() + imgs, err := m.multiAccounts.GetIdentityImages(keyUID) if err != nil { return nil, err } diff --git a/protocol/messenger_config.go b/protocol/messenger_config.go index 9d4841731..db4d9e838 100644 --- a/protocol/messenger_config.go +++ b/protocol/messenger_config.go @@ -2,6 +2,7 @@ package protocol import ( "database/sql" + "github.com/status-im/status-go/multiaccounts" "go.uber.org/zap" @@ -30,6 +31,7 @@ type config struct { // The database instance has a higher priority. dbConfig dbConfig db *sql.DB + multiAccount *multiaccounts.Database verifyTransactionClient EthClient @@ -92,6 +94,13 @@ func WithDatabase(db *sql.DB) Option { } } +func WithMultiAccounts(ma *multiaccounts.Database) Option { + return func(c *config) error { + c.multiAccount = ma + return nil + } +} + func WithPushNotificationServerConfig(pushNotificationServerConfig *pushnotificationserver.Config) Option { return func(c *config) error { c.pushNotificationServerConfig = pushNotificationServerConfig diff --git a/services/ext/api.go b/services/ext/api.go index 5eea9248b..2ee66a472 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -3,8 +3,10 @@ package ext import ( "context" "encoding/hex" + "encoding/json" "errors" "fmt" + "github.com/status-im/status-go/images" "math/big" "time" @@ -583,6 +585,59 @@ func (api *PublicAPI) Echo(ctx context.Context, message string) (string, error) return message, nil } +// +// Profile Images +// + +// GetIdentityImages returns an array of json marshalled IdentityImages assigned to the user's identity +func (api *PublicAPI) GetIdentityImages(keyUid string) (string, error) { + iis, err := api.service.multiAccountsDB.GetIdentityImages(keyUid) + if err != nil { + return "", err + } + + js, err := json.Marshal(iis) + + return string(js), err +} + +// GetIdentityImage returns a json object representing the image with the given name +func (api *PublicAPI) GetIdentityImage(keyUid, name string) (string, error) { + ii, err := api.service.multiAccountsDB.GetIdentityImage(keyUid, name) + if err != nil { + return "", err + } + + js, err := json.Marshal(ii) + + return string(js), err +} + +// StoreIdentityImage takes the filepath of an image, crops it as per the rect coords and finally resizes the image. +// The resulting image(s) will be stored in the DB along with other user account information. +// aX and aY represent the pixel coordinates of the upper left corner of the image's cropping area +// bX and bY represent the pixel coordinates of the lower right corner of the image's cropping area +func (api *PublicAPI) StoreIdentityImage(keyUid, filepath string, aX, aY, bX, bY int) (string, error) { + iis, err := images.GenerateIdentityImages(filepath, aX, aY, bX, bY) + if err != nil { + return "", err + } + + err = api.service.multiAccountsDB.StoreIdentityImages(keyUid, iis) + if err != nil { + return "", err + } + + js, err := json.Marshal(iis) + + return string(js), err +} + +// DeleteIdentityImage deletes an IdentityImage from the db with the given name +func (api *PublicAPI) DeleteIdentityImage(keyUid string) error { + return api.service.multiAccountsDB.DeleteIdentityImage(keyUid) +} + // ----- // HELPER // ----- diff --git a/services/ext/service.go b/services/ext/service.go index 701b0e3fa..5809daadd 100644 --- a/services/ext/service.go +++ b/services/ext/service.go @@ -4,6 +4,7 @@ import ( "context" "crypto/ecdsa" "database/sql" + "github.com/status-im/status-go/multiaccounts" "math/big" "os" "path/filepath" @@ -70,6 +71,7 @@ type Service struct { connManager *mailservers.ConnectionManager lastUsedMonitor *mailservers.LastUsedConnectionMonitor accountsDB *accounts.Database + multiAccountsDB *multiaccounts.Database } // Make sure that Service implements node.Service interface. @@ -115,7 +117,7 @@ func (s *Service) GetPeer(rawURL string) (*enode.Node, error) { return enode.ParseV4(rawURL) } -func (s *Service) InitProtocol(identity *ecdsa.PrivateKey, db *sql.DB, logger *zap.Logger) error { +func (s *Service) InitProtocol(identity *ecdsa.PrivateKey, db *sql.DB, multiAccountDb *multiaccounts.Database, logger *zap.Logger) error { if !s.config.PFSEnabled { return nil } @@ -147,8 +149,9 @@ func (s *Service) InitProtocol(identity *ecdsa.PrivateKey, db *sql.DB, logger *z Logger: logger, } s.accountsDB = accounts.NewDB(db) + s.multiAccountsDB = multiAccountDb - options, err := buildMessengerOptions(s.config, identity, db, envelopesMonitorConfig, s.accountsDB, logger) + options, err := buildMessengerOptions(s.config, identity, db, s.multiAccountsDB, envelopesMonitorConfig, s.accountsDB, logger) if err != nil { return err } @@ -454,6 +457,7 @@ func buildMessengerOptions( config params.ShhextConfig, identity *ecdsa.PrivateKey, db *sql.DB, + multiAccounts *multiaccounts.Database, envelopesMonitorConfig *transport.EnvelopesMonitorConfig, accountsDB *accounts.Database, logger *zap.Logger, @@ -462,6 +466,7 @@ func buildMessengerOptions( protocol.WithCustomLogger(logger), protocol.WithPushNotifications(), protocol.WithDatabase(db), + protocol.WithMultiAccounts(multiAccounts), protocol.WithEnvelopesMonitorConfig(envelopesMonitorConfig), protocol.WithOnNegotiatedFilters(onNegotiatedFilters), } diff --git a/services/wakuext/api_test.go b/services/wakuext/api_test.go index 71231602d..dff239d3d 100644 --- a/services/wakuext/api_test.go +++ b/services/wakuext/api_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/hex" "fmt" + "github.com/status-im/status-go/multiaccounts" "io/ioutil" "math" "net" @@ -129,7 +130,12 @@ func TestInitProtocol(t *testing.T) { sqlDB, err := appdatabase.InitializeDB(fmt.Sprintf("%s/db.sql", tmpdir), "password") require.NoError(t, err) - err = service.InitProtocol(privateKey, sqlDB, zap.NewNop()) + tmpfile, err := ioutil.TempFile("", "multi-accounts-tests-") + require.NoError(t, err) + multiAccounts, err := multiaccounts.InitializeDB(tmpfile.Name()) + require.NoError(t, err) + + err = service.InitProtocol(privateKey, sqlDB, multiAccounts, zap.NewNop()) require.NoError(t, err) } @@ -181,10 +187,18 @@ func (s *ShhExtSuite) createAndAddNode() { service := New(config, nodeWrapper, nil, nil, db) sqlDB, err := appdatabase.InitializeDB(fmt.Sprintf("%s/%d", s.dir, idx), "password") s.Require().NoError(err) + + tmpfile, err := ioutil.TempFile("", "multi-accounts-tests-") + s.Require().NoError(err) + multiAccounts, err := multiaccounts.InitializeDB(tmpfile.Name()) + s.Require().NoError(err) + privateKey, err := crypto.GenerateKey() s.NoError(err) - err = service.InitProtocol(privateKey, sqlDB, zap.NewNop()) + + err = service.InitProtocol(privateKey, sqlDB, multiAccounts, zap.NewNop()) s.NoError(err) + err = stack.Register(func(n *node.ServiceContext) (node.Service, error) { return service, nil })