diff --git a/PUSH-NOTIFICATIONS-TODO.txt b/PUSH-NOTIFICATIONS-TODO.txt new file mode 100644 index 000000000..4e3af4fdf --- /dev/null +++ b/PUSH-NOTIFICATIONS-TODO.txt @@ -0,0 +1,6 @@ +Specs changes: +- Use application metadata wrapper +- Encrypt of payload instead of signature + public key of server +- Removed preferencs + each device registers individually +- Add grant +- Add version in PushNotificationInfo diff --git a/VERSION b/VERSION index 9dd7871a6..2f4c74eb2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.55.3 +0.56.0 diff --git a/appdatabase/migrations/bindata.go b/appdatabase/migrations/bindata.go index 6b18606ae..d1a9cd1df 100644 --- a/appdatabase/migrations/bindata.go +++ b/appdatabase/migrations/bindata.go @@ -12,6 +12,7 @@ // 0005_waku_mode.up.sql (146B) // 0006_appearance.up.sql (67B) // 0007_enable_waku_default.up.sql (38B) +// 0008_add_push_notifications.up.sql (349B) // doc.go (74B) package migrations @@ -216,7 +217,7 @@ func _0004_pending_stickersDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "0004_pending_stickers.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1584434371, 0)} + info := bindataFileInfo{name: "0004_pending_stickers.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}} return a, nil } @@ -236,7 +237,7 @@ func _0004_pending_stickersUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "0004_pending_stickers.up.sql", size: 61, mode: os.FileMode(0644), modTime: time.Unix(1584434371, 0)} + info := bindataFileInfo{name: "0004_pending_stickers.up.sql", size: 61, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x3c, 0xed, 0x25, 0xdf, 0x75, 0x2, 0x6c, 0xf0, 0xa2, 0xa8, 0x37, 0x62, 0x65, 0xad, 0xfd, 0x98, 0xa0, 0x9d, 0x63, 0x94, 0xdf, 0x6b, 0x46, 0xe0, 0x68, 0xec, 0x9c, 0x7f, 0x77, 0xdd, 0xb3, 0x6}} return a, nil } @@ -256,7 +257,7 @@ func _0005_waku_modeDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "0005_waku_mode.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1584434371, 0)} + info := bindataFileInfo{name: "0005_waku_mode.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}} return a, nil } @@ -276,7 +277,7 @@ func _0005_waku_modeUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "0005_waku_mode.up.sql", size: 146, mode: os.FileMode(0644), modTime: time.Unix(1584434371, 0)} + info := bindataFileInfo{name: "0005_waku_mode.up.sql", size: 146, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xa6, 0x91, 0xc, 0xd7, 0x89, 0x61, 0x2e, 0x4c, 0x5a, 0xb6, 0x67, 0xd1, 0xc1, 0x42, 0x24, 0x38, 0xd6, 0x1b, 0x75, 0x41, 0x9c, 0x23, 0xb0, 0xca, 0x5c, 0xf1, 0x5c, 0xd0, 0x13, 0x92, 0x3e, 0xe1}} return a, nil } @@ -296,7 +297,7 @@ func _0006_appearanceUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "0006_appearance.up.sql", size: 67, mode: os.FileMode(0644), modTime: time.Unix(1585895847, 0)} + info := bindataFileInfo{name: "0006_appearance.up.sql", size: 67, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xae, 0x6, 0x25, 0x6c, 0xe4, 0x9d, 0xa7, 0x72, 0xe8, 0xbc, 0xe4, 0x1f, 0x1e, 0x2d, 0x7c, 0xb7, 0xf6, 0xa3, 0xec, 0x3b, 0x4e, 0x93, 0x2e, 0xa4, 0xec, 0x6f, 0xe5, 0x95, 0x94, 0xe8, 0x4, 0xfb}} return a, nil } @@ -316,11 +317,31 @@ func _0007_enable_waku_defaultUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "0007_enable_waku_default.up.sql", size: 38, mode: os.FileMode(0644), modTime: time.Unix(1585895900, 0)} + info := bindataFileInfo{name: "0007_enable_waku_default.up.sql", size: 38, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xd4, 0x42, 0xb6, 0xe5, 0x48, 0x41, 0xeb, 0xc0, 0x7e, 0x3b, 0xe6, 0x8e, 0x96, 0x33, 0x20, 0x92, 0x24, 0x5a, 0x60, 0xfa, 0xa0, 0x3, 0x5e, 0x76, 0x4b, 0x89, 0xaa, 0x37, 0x66, 0xbc, 0x26, 0x11}} return a, nil } +var __0008_add_push_notificationsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xa4\xce\x51\x0e\x82\x30\x0c\x00\xd0\x7f\x4f\xd1\x7b\xf8\x35\x64\x7c\x55\x48\x70\x7c\x2f\x08\x45\x97\x40\x6b\xd6\x6a\xe2\xed\x3d\x80\x33\x9a\x78\x81\x97\xe7\x30\xf8\x1e\x82\xab\xd0\x83\x92\x59\xe2\x8b\x82\xab\x6b\x38\x74\x38\x1c\x5b\xc8\xb4\x89\x51\xbc\xdd\xf5\x1a\x59\x2c\x2d\x69\x1a\x2d\x09\x6b\x24\x1e\xcf\x2b\xcd\x50\x75\x1d\x7a\xd7\x42\xed\x1b\x37\x60\x80\xc6\xe1\xc9\xef\x77\xdf\x60\x25\x9e\x0b\xec\x1b\x17\xfa\xe1\x07\xad\xf0\x53\xca\x0f\xca\xff\x36\x0b\xf0\x92\x65\x8b\x93\xb0\x8d\x93\x69\x14\x5e\x9f\x9f\xf0\x57\x00\x00\x00\xff\xff\x30\xc0\x56\xbd\x5d\x01\x00\x00") + +func _0008_add_push_notificationsUpSqlBytes() ([]byte, error) { + return bindataRead( + __0008_add_push_notificationsUpSql, + "0008_add_push_notifications.up.sql", + ) +} + +func _0008_add_push_notificationsUpSql() (*asset, error) { + bytes, err := _0008_add_push_notificationsUpSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "0008_add_push_notifications.up.sql", size: 349, mode: os.FileMode(0644), modTime: time.Unix(1595832401, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x5a, 0x0, 0xbf, 0xd0, 0xdd, 0xcd, 0x73, 0xe0, 0x7c, 0x56, 0xef, 0xdc, 0x57, 0x61, 0x94, 0x64, 0x70, 0xb9, 0xfa, 0xa1, 0x2a, 0x36, 0xc, 0x2f, 0xf8, 0x95, 0xa, 0x57, 0x3e, 0x7a, 0xd7, 0x12}} + return a, nil +} + var _docGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2c\xc9\xb1\x0d\xc4\x20\x0c\x05\xd0\x9e\x29\xfe\x02\xd8\xfd\x6d\xe3\x4b\xac\x2f\x44\x82\x09\x78\x7f\xa5\x49\xfd\xa6\x1d\xdd\xe8\xd8\xcf\x55\x8a\x2a\xe3\x47\x1f\xbe\x2c\x1d\x8c\xfa\x6f\xe3\xb4\x34\xd4\xd9\x89\xbb\x71\x59\xb6\x18\x1b\x35\x20\xa2\x9f\x0a\x03\xa2\xe5\x0d\x00\x00\xff\xff\x60\xcd\x06\xbe\x4a\x00\x00\x00") func docGoBytes() ([]byte, error) { @@ -456,6 +477,8 @@ var _bindata = map[string]func() (*asset, error){ "0007_enable_waku_default.up.sql": _0007_enable_waku_defaultUpSql, + "0008_add_push_notifications.up.sql": _0008_add_push_notificationsUpSql, + "doc.go": docGo, } @@ -500,19 +523,20 @@ type bintree struct { } var _bintree = &bintree{nil, map[string]*bintree{ - "0001_app.down.sql": &bintree{_0001_appDownSql, map[string]*bintree{}}, - "0001_app.up.sql": &bintree{_0001_appUpSql, map[string]*bintree{}}, - "0002_tokens.down.sql": &bintree{_0002_tokensDownSql, map[string]*bintree{}}, - "0002_tokens.up.sql": &bintree{_0002_tokensUpSql, map[string]*bintree{}}, - "0003_settings.down.sql": &bintree{_0003_settingsDownSql, map[string]*bintree{}}, - "0003_settings.up.sql": &bintree{_0003_settingsUpSql, map[string]*bintree{}}, - "0004_pending_stickers.down.sql": &bintree{_0004_pending_stickersDownSql, map[string]*bintree{}}, - "0004_pending_stickers.up.sql": &bintree{_0004_pending_stickersUpSql, map[string]*bintree{}}, - "0005_waku_mode.down.sql": &bintree{_0005_waku_modeDownSql, map[string]*bintree{}}, - "0005_waku_mode.up.sql": &bintree{_0005_waku_modeUpSql, map[string]*bintree{}}, - "0006_appearance.up.sql": &bintree{_0006_appearanceUpSql, map[string]*bintree{}}, - "0007_enable_waku_default.up.sql": &bintree{_0007_enable_waku_defaultUpSql, map[string]*bintree{}}, - "doc.go": &bintree{docGo, map[string]*bintree{}}, + "0001_app.down.sql": &bintree{_0001_appDownSql, map[string]*bintree{}}, + "0001_app.up.sql": &bintree{_0001_appUpSql, map[string]*bintree{}}, + "0002_tokens.down.sql": &bintree{_0002_tokensDownSql, map[string]*bintree{}}, + "0002_tokens.up.sql": &bintree{_0002_tokensUpSql, map[string]*bintree{}}, + "0003_settings.down.sql": &bintree{_0003_settingsDownSql, map[string]*bintree{}}, + "0003_settings.up.sql": &bintree{_0003_settingsUpSql, map[string]*bintree{}}, + "0004_pending_stickers.down.sql": &bintree{_0004_pending_stickersDownSql, map[string]*bintree{}}, + "0004_pending_stickers.up.sql": &bintree{_0004_pending_stickersUpSql, map[string]*bintree{}}, + "0005_waku_mode.down.sql": &bintree{_0005_waku_modeDownSql, map[string]*bintree{}}, + "0005_waku_mode.up.sql": &bintree{_0005_waku_modeUpSql, map[string]*bintree{}}, + "0006_appearance.up.sql": &bintree{_0006_appearanceUpSql, map[string]*bintree{}}, + "0007_enable_waku_default.up.sql": &bintree{_0007_enable_waku_defaultUpSql, map[string]*bintree{}}, + "0008_add_push_notifications.up.sql": &bintree{_0008_add_push_notificationsUpSql, map[string]*bintree{}}, + "doc.go": &bintree{docGo, map[string]*bintree{}}, }} // RestoreAsset restores an asset under the given directory. diff --git a/appdatabase/migrations/sql/0008_add_push_notifications.up.sql b/appdatabase/migrations/sql/0008_add_push_notifications.up.sql new file mode 100644 index 000000000..af74b8a37 --- /dev/null +++ b/appdatabase/migrations/sql/0008_add_push_notifications.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE settings ADD COLUMN remote_push_notifications_enabled BOOLEAN DEFAULT FALSE; +ALTER TABLE settings ADD COLUMN send_push_notifications BOOLEAN DEFAULT TRUE; +ALTER TABLE settings ADD COLUMN push_notifications_server_enabled BOOLEAN DEFAULT FALSE; +ALTER TABLE settings ADD COLUMN push_notifications_from_contacts_only BOOLEAN DEFAULT FALSE; diff --git a/multiaccounts/accounts/database.go b/multiaccounts/accounts/database.go index ce142d5c2..740893a6d 100644 --- a/multiaccounts/accounts/database.go +++ b/multiaccounts/accounts/database.go @@ -59,25 +59,34 @@ type Settings struct { Mnemonic *string `json:"mnemonic,omitempty"` Name string `json:"name,omitempty"` Networks *json.RawMessage `json:"networks/networks"` - NotificationsEnabled bool `json:"notifications-enabled?,omitempty"` - PhotoPath string `json:"photo-path"` - PinnedMailserver *json.RawMessage `json:"pinned-mailservers,omitempty"` - PreferredName *string `json:"preferred-name,omitempty"` - PreviewPrivacy bool `json:"preview-privacy?"` - PublicKey string `json:"public-key"` - RememberSyncingChoice bool `json:"remember-syncing-choice?,omitempty"` - SigningPhrase string `json:"signing-phrase"` - StickerPacksInstalled *json.RawMessage `json:"stickers/packs-installed,omitempty"` - StickerPacksPending *json.RawMessage `json:"stickers/packs-pending,omitempty"` - StickersRecentStickers *json.RawMessage `json:"stickers/recent-stickers,omitempty"` - SyncingOnMobileNetwork bool `json:"syncing-on-mobile-network?,omitempty"` - Appearance uint `json:"appearance"` - Usernames *json.RawMessage `json:"usernames,omitempty"` - WalletRootAddress types.Address `json:"wallet-root-address,omitempty"` - WalletSetUpPassed bool `json:"wallet-set-up-passed?,omitempty"` - WalletVisibleTokens *json.RawMessage `json:"wallet/visible-tokens,omitempty"` - WakuEnabled bool `json:"waku-enabled,omitempty"` - WakuBloomFilterMode bool `json:"waku-bloom-filter-mode,omitempty"` + // NotificationsEnabled indicates whether local notifications should be enabled (android only) + NotificationsEnabled bool `json:"notifications-enabled?,omitempty"` + PhotoPath string `json:"photo-path"` + PinnedMailserver *json.RawMessage `json:"pinned-mailservers,omitempty"` + PreferredName *string `json:"preferred-name,omitempty"` + PreviewPrivacy bool `json:"preview-privacy?"` + PublicKey string `json:"public-key"` + // PushNotificationsServerEnabled indicates whether we should be running a push notification server + PushNotificationsServerEnabled bool `json:"push-notifications-server-enabled?,omitempty"` + // PushNotificationsFromContactsOnly indicates whether we should only receive push notifications from contacts + PushNotificationsFromContactsOnly bool `json:"push-notifications-from-contacts-only?,omitempty"` + RememberSyncingChoice bool `json:"remember-syncing-choice?,omitempty"` + // RemotePushNotificationsEnabled indicates whether we should be using remote notifications (ios only for now) + RemotePushNotificationsEnabled bool `json:"remote-push-notifications-enabled?,omitempty"` + SigningPhrase string `json:"signing-phrase"` + StickerPacksInstalled *json.RawMessage `json:"stickers/packs-installed,omitempty"` + StickerPacksPending *json.RawMessage `json:"stickers/packs-pending,omitempty"` + StickersRecentStickers *json.RawMessage `json:"stickers/recent-stickers,omitempty"` + SyncingOnMobileNetwork bool `json:"syncing-on-mobile-network?,omitempty"` + // SendPushNotifications indicates whether we should send push notifications for other clients + SendPushNotifications bool `json:"send-push-notifications?,omitempty"` + Appearance uint `json:"appearance"` + Usernames *json.RawMessage `json:"usernames,omitempty"` + WalletRootAddress types.Address `json:"wallet-root-address,omitempty"` + WalletSetUpPassed bool `json:"wallet-set-up-passed?,omitempty"` + WalletVisibleTokens *json.RawMessage `json:"wallet/visible-tokens,omitempty"` + WakuEnabled bool `json:"waku-enabled,omitempty"` + WakuBloomFilterMode bool `json:"waku-bloom-filter-mode,omitempty"` } func NewDB(db *sql.DB) *Database { @@ -246,6 +255,30 @@ func (db *Database) SaveSetting(setting string, value interface{}) error { return ErrInvalidConfig } update, err = db.db.Prepare("UPDATE settings SET remember_syncing_choice = ? WHERE synthetic_id = 'id'") + case "remote-push-notifications-enabled?": + _, ok := value.(bool) + if !ok { + return ErrInvalidConfig + } + update, err = db.db.Prepare("UPDATE settings SET remote_push_notifications_enabled = ? WHERE synthetic_id = 'id'") + case "push-notifications-server-enabled?": + _, ok := value.(bool) + if !ok { + return ErrInvalidConfig + } + update, err = db.db.Prepare("UPDATE settings SET push_notifications_server_enabled = ? WHERE synthetic_id = 'id'") + case "push-notifications-from-contacts-only?": + _, ok := value.(bool) + if !ok { + return ErrInvalidConfig + } + update, err = db.db.Prepare("UPDATE settings SET push_notifications_from_contacts_only = ? WHERE synthetic_id = 'id'") + case "send-push-notifications?": + _, ok := value.(bool) + if !ok { + return ErrInvalidConfig + } + update, err = db.db.Prepare("UPDATE settings SET send_push_notifications = ? WHERE synthetic_id = 'id'") case "stickers/packs-installed": value = &sqlite.JSONBlob{value} update, err = db.db.Prepare("UPDATE settings SET stickers_packs_installed = ? WHERE synthetic_id = 'id'") @@ -304,7 +337,7 @@ func (db *Database) GetNodeConfig(nodecfg interface{}) error { func (db *Database) GetSettings() (Settings, error) { var s Settings - err := db.db.QueryRow("SELECT address, chaos_mode, currency, current_network, custom_bootnodes, custom_bootnodes_enabled, dapps_address, eip1581_address, fleet, hide_home_tooltip, installation_id, key_uid, keycard_instance_uid, keycard_paired_on, keycard_pairing, last_updated, latest_derived_path, log_level, mnemonic, name, networks, notifications_enabled, photo_path, pinned_mailservers, preferred_name, preview_privacy, public_key, remember_syncing_choice, signing_phrase, stickers_packs_installed, stickers_packs_pending, stickers_recent_stickers, syncing_on_mobile_network, usernames, appearance, wallet_root_address, wallet_set_up_passed, wallet_visible_tokens, waku_enabled, waku_bloom_filter_mode FROM settings WHERE synthetic_id = 'id'").Scan( + err := db.db.QueryRow("SELECT address, chaos_mode, currency, current_network, custom_bootnodes, custom_bootnodes_enabled, dapps_address, eip1581_address, fleet, hide_home_tooltip, installation_id, key_uid, keycard_instance_uid, keycard_paired_on, keycard_pairing, last_updated, latest_derived_path, log_level, mnemonic, name, networks, notifications_enabled, push_notifications_server_enabled, push_notifications_from_contacts_only, remote_push_notifications_enabled, send_push_notifications, photo_path, pinned_mailservers, preferred_name, preview_privacy, public_key, remember_syncing_choice, signing_phrase, stickers_packs_installed, stickers_packs_pending, stickers_recent_stickers, syncing_on_mobile_network, usernames, appearance, wallet_root_address, wallet_set_up_passed, wallet_visible_tokens, waku_enabled, waku_bloom_filter_mode FROM settings WHERE synthetic_id = 'id'").Scan( &s.Address, &s.ChaosMode, &s.Currency, @@ -327,6 +360,10 @@ func (db *Database) GetSettings() (Settings, error) { &s.Name, &s.Networks, &s.NotificationsEnabled, + &s.PushNotificationsServerEnabled, + &s.PushNotificationsFromContactsOnly, + &s.RemotePushNotificationsEnabled, + &s.SendPushNotifications, &s.PhotoPath, &s.PinnedMailserver, &s.PreferredName, diff --git a/multiaccounts/accounts/database_test.go b/multiaccounts/accounts/database_test.go index 6d26761ba..2725d821e 100644 --- a/multiaccounts/accounts/database_test.go +++ b/multiaccounts/accounts/database_test.go @@ -22,19 +22,20 @@ var ( networks = json.RawMessage("{}") settings = Settings{ - Address: types.HexToAddress("0xdC540f3745Ff2964AFC1171a5A0DD726d1F6B472"), - CurrentNetwork: "mainnet_rpc", - DappsAddress: types.HexToAddress("0xD1300f99fDF7346986CbC766903245087394ecd0"), - InstallationID: "d3efcff6-cffa-560e-a547-21d3858cbc51", - KeyUID: "0x4e8129f3edfc004875be17bf468a784098a9f69b53c095be1f52deff286935ab", - LatestDerivedPath: 0, - Name: "Jittery Cornflowerblue Kingbird", - Networks: &networks, - PhotoPath: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAjklEQVR4nOzXwQmFMBAAUZXUYh32ZB32ZB02sxYQQSZGsod55/91WFgSS0RM+SyjA56ZRZhFmEWYRRT6h+M6G16zrxv6fdJpmUWYRbxsYr13dKfanpN0WmYRZhGzXz6AWYRZRIfbaX26fT9Jk07LLMIsosPt9I/dTDotswizCG+nhFmEWYRZhFnEHQAA///z1CFkYamgfQAAAABJRU5ErkJggg==", - PreviewPrivacy: false, - PublicKey: "0x04211fe0f69772ecf7eb0b5bfc7678672508a9fb01f2d699096f0d59ef7fe1a0cb1e648a80190db1c0f5f088872444d846f2956d0bd84069f3f9f69335af852ac0", - SigningPhrase: "yurt joey vibe", - WalletRootAddress: types.HexToAddress("0x3B591fd819F86D0A6a2EF2Bcb94f77807a7De1a6")} + Address: types.HexToAddress("0xdC540f3745Ff2964AFC1171a5A0DD726d1F6B472"), + CurrentNetwork: "mainnet_rpc", + DappsAddress: types.HexToAddress("0xD1300f99fDF7346986CbC766903245087394ecd0"), + InstallationID: "d3efcff6-cffa-560e-a547-21d3858cbc51", + KeyUID: "0x4e8129f3edfc004875be17bf468a784098a9f69b53c095be1f52deff286935ab", + LatestDerivedPath: 0, + Name: "Jittery Cornflowerblue Kingbird", + Networks: &networks, + PhotoPath: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAIAAACRXR/mAAAAjklEQVR4nOzXwQmFMBAAUZXUYh32ZB32ZB02sxYQQSZGsod55/91WFgSS0RM+SyjA56ZRZhFmEWYRRT6h+M6G16zrxv6fdJpmUWYRbxsYr13dKfanpN0WmYRZhGzXz6AWYRZRIfbaX26fT9Jk07LLMIsosPt9I/dTDotswizCG+nhFmEWYRZhFnEHQAA///z1CFkYamgfQAAAABJRU5ErkJggg==", + PreviewPrivacy: false, + PublicKey: "0x04211fe0f69772ecf7eb0b5bfc7678672508a9fb01f2d699096f0d59ef7fe1a0cb1e648a80190db1c0f5f088872444d846f2956d0bd84069f3f9f69335af852ac0", + SigningPhrase: "yurt joey vibe", + SendPushNotifications: true, + WalletRootAddress: types.HexToAddress("0x3B591fd819F86D0A6a2EF2Bcb94f77807a7De1a6")} ) func setupTestDB(t *testing.T) (*Database, func()) { diff --git a/peers/peerpool_test.go b/peers/peerpool_test.go index d8b8b8b26..45e7018e8 100644 --- a/peers/peerpool_test.go +++ b/peers/peerpool_test.go @@ -30,6 +30,21 @@ import ( "github.com/status-im/status-go/whisper/v6" ) +// GetFreePort asks the kernel for a free open port that is ready to use. +func getFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + type PeerPoolSimulationSuite struct { suite.Suite @@ -41,14 +56,20 @@ type PeerPoolSimulationSuite struct { } func TestPeerPoolSimulationSuite(t *testing.T) { - s := new(PeerPoolSimulationSuite) - s.port = 33731 + s := &PeerPoolSimulationSuite{} + port, err := getFreePort() + if err != nil { + panic(err) + } + s.port = uint16(port) + suite.Run(t, s) } func (s *PeerPoolSimulationSuite) nextPort() uint16 { - s.port++ - return s.port + port, err := getFreePort() + s.Require().NoError(err) + return uint16(port) } func (s *PeerPoolSimulationSuite) SetupTest() { diff --git a/protocol/applicationmetadata/message.pb.go b/protocol/applicationmetadata/message.pb.go deleted file mode 100644 index 4e71857d8..000000000 --- a/protocol/applicationmetadata/message.pb.go +++ /dev/null @@ -1,85 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// source: message.proto - -package applicationmetadata - -import ( - fmt "fmt" - proto "github.com/golang/protobuf/proto" - math "math" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package - -type Message struct { - Signature []byte `protobuf:"bytes,4001,opt,name=signature,proto3" json:"signature,omitempty"` - Payload []byte `protobuf:"bytes,4002,opt,name=payload,proto3" json:"payload,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *Message) Reset() { *m = Message{} } -func (m *Message) String() string { return proto.CompactTextString(m) } -func (*Message) ProtoMessage() {} -func (*Message) Descriptor() ([]byte, []int) { - return fileDescriptor_33c57e4bae7b9afd, []int{0} -} - -func (m *Message) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_Message.Unmarshal(m, b) -} -func (m *Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_Message.Marshal(b, m, deterministic) -} -func (m *Message) XXX_Merge(src proto.Message) { - xxx_messageInfo_Message.Merge(m, src) -} -func (m *Message) XXX_Size() int { - return xxx_messageInfo_Message.Size(m) -} -func (m *Message) XXX_DiscardUnknown() { - xxx_messageInfo_Message.DiscardUnknown(m) -} - -var xxx_messageInfo_Message proto.InternalMessageInfo - -func (m *Message) GetSignature() []byte { - if m != nil { - return m.Signature - } - return nil -} - -func (m *Message) GetPayload() []byte { - if m != nil { - return m.Payload - } - return nil -} - -func init() { - proto.RegisterType((*Message)(nil), "applicationmetadata.Message") -} - -func init() { proto.RegisterFile("message.proto", fileDescriptor_33c57e4bae7b9afd) } - -var fileDescriptor_33c57e4bae7b9afd = []byte{ - // 112 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xcd, 0x4d, 0x2d, 0x2e, - 0x4e, 0x4c, 0x4f, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x4e, 0x2c, 0x28, 0xc8, 0xc9, - 0x4c, 0x4e, 0x2c, 0xc9, 0xcc, 0xcf, 0xcb, 0x4d, 0x2d, 0x49, 0x4c, 0x49, 0x2c, 0x49, 0x54, 0x72, - 0xe6, 0x62, 0xf7, 0x85, 0xa8, 0x12, 0x92, 0xe5, 0xe2, 0x2c, 0xce, 0x4c, 0xcf, 0x4b, 0x2c, 0x29, - 0x2d, 0x4a, 0x95, 0x58, 0x28, 0xaf, 0xc0, 0xa8, 0xc1, 0x13, 0x84, 0x10, 0x11, 0x92, 0xe4, 0x62, - 0x2f, 0x48, 0xac, 0xcc, 0xc9, 0x4f, 0x4c, 0x91, 0x58, 0x04, 0x91, 0x84, 0xf1, 0x93, 0xd8, 0xc0, - 0x16, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xb0, 0x7f, 0x4a, 0x96, 0x71, 0x00, 0x00, 0x00, -} diff --git a/protocol/applicationmetadata/message.proto b/protocol/applicationmetadata/message.proto deleted file mode 100644 index 009830697..000000000 --- a/protocol/applicationmetadata/message.proto +++ /dev/null @@ -1,8 +0,0 @@ -syntax = "proto3"; - -package applicationmetadata; - -message Message { - bytes signature = 4001; - bytes payload = 4002; -} diff --git a/protocol/applicationmetadata/pubkey.go b/protocol/applicationmetadata/pubkey.go deleted file mode 100644 index 29a152598..000000000 --- a/protocol/applicationmetadata/pubkey.go +++ /dev/null @@ -1,23 +0,0 @@ -package applicationmetadata - -import ( - "crypto/ecdsa" - - "github.com/status-im/status-go/eth-node/crypto" -) - -func (m *Message) RecoverKey() (*ecdsa.PublicKey, error) { - if m.Signature == nil { - return nil, nil - } - - recoveredKey, err := crypto.SigToPub( - crypto.Keccak256(m.Payload), - m.Signature, - ) - if err != nil { - return nil, err - } - - return recoveredKey, nil -} diff --git a/protocol/applicationmetadata/service.go b/protocol/applicationmetadata/service.go deleted file mode 100644 index b3a23b48a..000000000 --- a/protocol/applicationmetadata/service.go +++ /dev/null @@ -1,17 +0,0 @@ -package applicationmetadata - -import ( - "github.com/golang/protobuf/proto" -) - -//go:generate protoc --go_out=. ./message.proto - -func Unmarshal(payload []byte) (*Message, error) { - var message Message - err := proto.Unmarshal(payload, &message) - if err != nil { - return nil, err - } - - return &message, nil -} diff --git a/protocol/chat.go b/protocol/chat.go index 846324ffb..dcd6124b9 100644 --- a/protocol/chat.go +++ b/protocol/chat.go @@ -64,6 +64,10 @@ type Chat struct { Alias string `json:"alias,omitempty"` // Identicon generated from public key Identicon string `json:"identicon"` + + // Muted is used to check whether we want to receive + // push notifications for this chat + Muted bool `json:"muted,omitempty"` } func (c *Chat) PublicKey() (*ecdsa.PublicKey, error) { diff --git a/protocol/common/crypto.go b/protocol/common/crypto.go new file mode 100644 index 000000000..0e0a101c8 --- /dev/null +++ b/protocol/common/crypto.go @@ -0,0 +1,71 @@ +package common + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "errors" + "io" + + "golang.org/x/crypto/sha3" + + "github.com/status-im/status-go/eth-node/crypto" +) + +const nonceLength = 12 + +var ErrInvalidCiphertextLength = errors.New("invalid cyphertext length") + +func HashPublicKey(pk *ecdsa.PublicKey) []byte { + return Shake256(crypto.CompressPubkey(pk)) +} + +func Decrypt(cyphertext []byte, key []byte) ([]byte, error) { + if len(cyphertext) < nonceLength { + return nil, ErrInvalidCiphertextLength + } + + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonce := cyphertext[:nonceLength] + return gcm.Open(nil, nonce, cyphertext[nonceLength:], nil) +} + +func Encrypt(plaintext []byte, key []byte, reader io.Reader) ([]byte, error) { + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(reader, nonce); err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, plaintext, nil), nil +} + +func Shake256(buf []byte) []byte { + h := make([]byte, 64) + sha3.ShakeSum256(h, buf) + return h +} + +// IsPubKeyEqual checks that two public keys are equal +func IsPubKeyEqual(a, b *ecdsa.PublicKey) bool { + // the curve is always the same, just compare the points + return a.X.Cmp(b.X) == 0 && a.Y.Cmp(b.Y) == 0 +} diff --git a/protocol/common/feature_flags.go b/protocol/common/feature_flags.go new file mode 100644 index 000000000..9473e1da1 --- /dev/null +++ b/protocol/common/feature_flags.go @@ -0,0 +1,8 @@ +package common + +type FeatureFlags struct { + // Datasync indicates whether direct messages should be sent exclusively + // using datasync, breaking change for non-v1 clients. Public messages + // are not impacted + Datasync bool +} diff --git a/protocol/message_processor.go b/protocol/common/message_processor.go similarity index 56% rename from protocol/message_processor.go rename to protocol/common/message_processor.go index 548534861..aa96a2a73 100644 --- a/protocol/message_processor.go +++ b/protocol/common/message_processor.go @@ -1,9 +1,10 @@ -package protocol +package common import ( "context" "crypto/ecdsa" "database/sql" + "sync" "time" "github.com/golang/protobuf/proto" @@ -34,24 +35,41 @@ const ( whisperPoWTime = 5 ) -type messageProcessor struct { +// SentMessage reprent a message that has been passed to the transport layer +type SentMessage struct { + PublicKey *ecdsa.PublicKey + Spec *encryption.ProtocolMessageSpec + MessageIDs [][]byte +} + +type MessageProcessor struct { identity *ecdsa.PrivateKey datasync *datasync.DataSync protocol *encryption.Protocol transport transport.Transport logger *zap.Logger - featureFlags featureFlags + // ephemeralKeys is a map that contains the ephemeral keys of the client, used + // to decrypt messages + ephemeralKeys map[string]*ecdsa.PrivateKey + ephemeralKeysMutex sync.Mutex + + // sentMessagesSubscriptions contains all the subscriptions for sent messages + sentMessagesSubscriptions []chan<- *SentMessage + // sentMessagesSubscriptions contains all the subscriptions for scheduled messages + scheduledMessagesSubscriptions []chan<- *RawMessage + + featureFlags FeatureFlags } -func newMessageProcessor( +func NewMessageProcessor( identity *ecdsa.PrivateKey, database *sql.DB, enc *encryption.Protocol, transport transport.Transport, logger *zap.Logger, - features featureFlags, -) (*messageProcessor, error) { + features FeatureFlags, +) (*MessageProcessor, error) { dataSyncTransport := datasync.NewNodeTransport() dataSyncNode, err := datasyncnode.NewPersistentNode( database, @@ -64,22 +82,23 @@ func newMessageProcessor( if err != nil { return nil, err } - ds := datasync.New(dataSyncNode, dataSyncTransport, features.datasync, logger) + ds := datasync.New(dataSyncNode, dataSyncTransport, features.Datasync, logger) - p := &messageProcessor{ - identity: identity, - datasync: ds, - protocol: enc, - transport: transport, - logger: logger, - featureFlags: features, + p := &MessageProcessor{ + identity: identity, + datasync: ds, + protocol: enc, + transport: transport, + logger: logger, + ephemeralKeys: make(map[string]*ecdsa.PrivateKey), + featureFlags: features, } // Initializing DataSync is required to encrypt and send messages. // With DataSync enabled, messages are added to the DataSync // but actual encrypt and send calls are postponed. // sendDataSync is responsible for encrypting and sending postponed messages. - if features.datasync { + if features.Datasync { ds.Init(p.sendDataSync) ds.Start(300 * time.Millisecond) } @@ -87,27 +106,42 @@ func newMessageProcessor( return p, nil } -func (p *messageProcessor) Stop() { +func (p *MessageProcessor) Stop() { + for _, c := range p.sentMessagesSubscriptions { + close(c) + } p.datasync.Stop() // idempotent op } // SendPrivate takes encoded data, encrypts it and sends through the wire. -func (p *messageProcessor) SendPrivate( +func (p *MessageProcessor) SendPrivate( ctx context.Context, recipient *ecdsa.PublicKey, rawMessage *RawMessage, ) ([]byte, error) { p.logger.Debug( "sending a private message", - zap.Binary("public-key", crypto.FromECDSAPub(recipient)), + zap.String("public-key", types.EncodeHex(crypto.FromECDSAPub(recipient))), zap.String("site", "SendPrivate"), ) + // Currently we don't support sending through datasync and setting custom waku fields, + // as the datasync interface is not rich enough to propagate that information, so we + // would have to add some complexity to handle this. + if rawMessage.ResendAutomatically && (rawMessage.Sender != nil || rawMessage.SkipEncryption) { + return nil, errors.New("setting identity, skip-encryption and datasync not supported") + } + + // Set sender identity if not specified + if rawMessage.Sender == nil { + rawMessage.Sender = p.identity + } + return p.sendPrivate(ctx, recipient, rawMessage) } -// SendGroupRaw takes encoded data, encrypts it and sends through the wire, +// SendGroup takes encoded data, encrypts it and sends through the wire, // always return the messageID -func (p *messageProcessor) SendGroup( +func (p *MessageProcessor) SendGroup( ctx context.Context, recipients []*ecdsa.PublicKey, rawMessage *RawMessage, @@ -116,12 +150,18 @@ func (p *messageProcessor) SendGroup( "sending a private group message", zap.String("site", "SendGroup"), ) - // Calculate messageID first + // Set sender if not specified + if rawMessage.Sender == nil { + rawMessage.Sender = p.identity + } + + // Calculate messageID first and set on raw message wrappedMessage, err := p.wrapMessageV1(rawMessage) if err != nil { return nil, errors.Wrap(err, "failed to wrap message") } - messageID := v1protocol.MessageID(&p.identity.PublicKey, wrappedMessage) + messageID := v1protocol.MessageID(&rawMessage.Sender.PublicKey, wrappedMessage) + rawMessage.ID = types.EncodeHex(messageID) // Send to each recipients for _, recipient := range recipients { @@ -134,51 +174,67 @@ func (p *messageProcessor) SendGroup( } // sendPrivate sends data to the recipient identifying with a given public key. -func (p *messageProcessor) sendPrivate( +func (p *MessageProcessor) sendPrivate( ctx context.Context, recipient *ecdsa.PublicKey, rawMessage *RawMessage, ) ([]byte, error) { - p.logger.Debug("sending private message", zap.Binary("recipient", crypto.FromECDSAPub(recipient))) + p.logger.Debug("sending private message", zap.String("recipient", types.EncodeHex(crypto.FromECDSAPub(recipient)))) wrappedMessage, err := p.wrapMessageV1(rawMessage) if err != nil { return nil, errors.Wrap(err, "failed to wrap message") } - messageID := v1protocol.MessageID(&p.identity.PublicKey, wrappedMessage) + messageID := v1protocol.MessageID(&rawMessage.Sender.PublicKey, wrappedMessage) + rawMessage.ID = types.EncodeHex(messageID) - if p.featureFlags.datasync { + // Notify before dispatching, otherwise the dispatch subscription might happen + // earlier than the scheduled + p.notifyOnScheduledMessage(rawMessage) + + if p.featureFlags.Datasync && rawMessage.ResendAutomatically { + // No need to call transport tracking. + // It is done in a data sync dispatch step. if err := p.addToDataSync(recipient, wrappedMessage); err != nil { return nil, errors.Wrap(err, "failed to send message with datasync") } - // No need to call transport tracking. - // It is done in a data sync dispatch step. - } else { - messageSpec, err := p.protocol.BuildDirectMessage(p.identity, recipient, wrappedMessage) - if err != nil { - return nil, errors.Wrap(err, "failed to encrypt message") - } - - hash, newMessage, err := p.sendMessageSpec(ctx, recipient, messageSpec) + } else if rawMessage.SkipEncryption { + // When SkipEncryption is set we don't pass the message to the encryption layer + messageIDs := [][]byte{messageID} + hash, newMessage, err := p.sendRawMessage(ctx, recipient, wrappedMessage, messageIDs) if err != nil { return nil, errors.Wrap(err, "failed to send a message spec") } - p.transport.Track([][]byte{messageID}, hash, newMessage) + p.transport.Track(messageIDs, hash, newMessage) + + } else { + messageSpec, err := p.protocol.BuildDirectMessage(rawMessage.Sender, recipient, wrappedMessage) + if err != nil { + return nil, errors.Wrap(err, "failed to encrypt message") + } + + messageIDs := [][]byte{messageID} + hash, newMessage, err := p.sendMessageSpec(ctx, recipient, messageSpec, messageIDs) + if err != nil { + return nil, errors.Wrap(err, "failed to send a message spec") + } + + p.transport.Track(messageIDs, hash, newMessage) } return messageID, nil } // sendPairInstallation sends data to the recipients, using DH -func (p *messageProcessor) SendPairInstallation( +func (p *MessageProcessor) SendPairInstallation( ctx context.Context, recipient *ecdsa.PublicKey, rawMessage *RawMessage, ) ([]byte, error) { - p.logger.Debug("sending private message", zap.Binary("recipient", crypto.FromECDSAPub(recipient))) + p.logger.Debug("sending private message", zap.String("recipient", types.EncodeHex(crypto.FromECDSAPub(recipient)))) wrappedMessage, err := p.wrapMessageV1(rawMessage) if err != nil { @@ -190,20 +246,22 @@ func (p *messageProcessor) SendPairInstallation( return nil, errors.Wrap(err, "failed to encrypt message") } - hash, newMessage, err := p.sendMessageSpec(ctx, recipient, messageSpec) + messageID := v1protocol.MessageID(&p.identity.PublicKey, wrappedMessage) + messageIDs := [][]byte{messageID} + + hash, newMessage, err := p.sendMessageSpec(ctx, recipient, messageSpec, messageIDs) if err != nil { return nil, errors.Wrap(err, "failed to send a message spec") } - messageID := v1protocol.MessageID(&p.identity.PublicKey, wrappedMessage) - p.transport.Track([][]byte{messageID}, hash, newMessage) + p.transport.Track(messageIDs, hash, newMessage) return messageID, nil } // EncodeMembershipUpdate takes a group and an optional chat message and returns the protobuf representation to be sent on the wire. // All the events in a group are encoded and added to the payload -func (p *messageProcessor) EncodeMembershipUpdate( +func (p *MessageProcessor) EncodeMembershipUpdate( group *v1protocol.Group, chatMessage *protobuf.ChatMessage, ) ([]byte, error) { @@ -222,43 +280,50 @@ func (p *messageProcessor) EncodeMembershipUpdate( } // SendPublic takes encoded data, encrypts it and sends through the wire. -func (p *messageProcessor) SendPublic( +func (p *MessageProcessor) SendPublic( ctx context.Context, chatName string, rawMessage *RawMessage, ) ([]byte, error) { - var newMessage *types.NewMessage + // Set sender + if rawMessage.Sender == nil { + rawMessage.Sender = p.identity + } wrappedMessage, err := p.wrapMessageV1(rawMessage) if err != nil { return nil, errors.Wrap(err, "failed to wrap message") } - newMessage = &types.NewMessage{ + newMessage := &types.NewMessage{ TTL: whisperTTL, Payload: wrappedMessage, PowTarget: calculatePoW(wrappedMessage), PowTime: whisperPoWTime, } + messageID := v1protocol.MessageID(&rawMessage.Sender.PublicKey, wrappedMessage) + rawMessage.ID = types.EncodeHex(messageID) + + // notify before dispatching + p.notifyOnScheduledMessage(rawMessage) + hash, err := p.transport.SendPublic(ctx, newMessage, chatName) if err != nil { return nil, err } - messageID := v1protocol.MessageID(&p.identity.PublicKey, wrappedMessage) - p.transport.Track([][]byte{messageID}, hash, newMessage) return messageID, nil } -// handleMessages expects a whisper message as input, and it will go through +// HandleMessages expects a whisper message as input, and it will go through // a series of transformations until the message is parsed into an application // layer message, or in case of Raw methods, the processing stops at the layer // before. // It returns an error only if the processing of required steps failed. -func (p *messageProcessor) handleMessages(shhMessage *types.Message, applicationLayer bool) ([]*v1protocol.StatusMessage, error) { +func (p *MessageProcessor) HandleMessages(shhMessage *types.Message, applicationLayer bool) ([]*v1protocol.StatusMessage, error) { logger := p.logger.With(zap.String("site", "handleMessages")) hlogger := logger.With(zap.ByteString("hash", shhMessage.Hash)) var statusMessage v1protocol.StatusMessage @@ -296,12 +361,32 @@ func (p *messageProcessor) handleMessages(shhMessage *types.Message, application return statusMessages, nil } -func (p *messageProcessor) handleEncryptionLayer(ctx context.Context, message *v1protocol.StatusMessage) error { +// fetchDecryptionKey returns the private key associated with this public key, and returns true if it's an ephemeral key +func (p *MessageProcessor) fetchDecryptionKey(destination *ecdsa.PublicKey) (*ecdsa.PrivateKey, bool) { + destinationID := types.EncodeHex(crypto.FromECDSAPub(destination)) + + p.ephemeralKeysMutex.Lock() + decryptionKey, ok := p.ephemeralKeys[destinationID] + p.ephemeralKeysMutex.Unlock() + + // the key is not there, fallback on identity + if !ok { + return p.identity, false + } + return decryptionKey, true +} + +func (p *MessageProcessor) handleEncryptionLayer(ctx context.Context, message *v1protocol.StatusMessage) error { logger := p.logger.With(zap.String("site", "handleEncryptionLayer")) publicKey := message.SigPubKey() - err := message.HandleEncryption(p.identity, publicKey, p.protocol) - if err == encryption.ErrDeviceNotFound { + // if it's an ephemeral key, we don't negotiate a topic + decryptionKey, skipNegotiation := p.fetchDecryptionKey(message.Dst) + + err := message.HandleEncryption(decryptionKey, publicKey, p.protocol, skipNegotiation) + + // if it's an ephemeral key, we don't have to handle a device not found error + if err == encryption.ErrDeviceNotFound && !skipNegotiation { if err := p.handleErrDeviceNotFound(ctx, publicKey); err != nil { logger.Error("failed to handle ErrDeviceNotFound", zap.Error(err)) } @@ -313,7 +398,7 @@ func (p *messageProcessor) handleEncryptionLayer(ctx context.Context, message *v return nil } -func (p *messageProcessor) handleErrDeviceNotFound(ctx context.Context, publicKey *ecdsa.PublicKey) error { +func (p *MessageProcessor) handleErrDeviceNotFound(ctx context.Context, publicKey *ecdsa.PublicKey) error { now := time.Now().Unix() advertise, err := p.protocol.ShouldAdvertiseBundle(publicKey, now) if err != nil { @@ -330,7 +415,9 @@ func (p *messageProcessor) handleErrDeviceNotFound(ctx context.Context, publicKe ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() - _, _, err = p.sendMessageSpec(ctx, publicKey, messageSpec) + // We don't pass an array of messageIDs as no action needs to be taken + // when sending a bundle + _, _, err = p.sendMessageSpec(ctx, publicKey, messageSpec, nil) if err != nil { return err } @@ -340,15 +427,15 @@ func (p *messageProcessor) handleErrDeviceNotFound(ctx context.Context, publicKe return nil } -func (p *messageProcessor) wrapMessageV1(rawMessage *RawMessage) ([]byte, error) { - wrappedMessage, err := v1protocol.WrapMessageV1(rawMessage.Payload, rawMessage.MessageType, p.identity) +func (p *MessageProcessor) wrapMessageV1(rawMessage *RawMessage) ([]byte, error) { + wrappedMessage, err := v1protocol.WrapMessageV1(rawMessage.Payload, rawMessage.MessageType, rawMessage.Sender) if err != nil { return nil, errors.Wrap(err, "failed to wrap message") } return wrappedMessage, nil } -func (p *messageProcessor) addToDataSync(publicKey *ecdsa.PublicKey, message []byte) error { +func (p *MessageProcessor) addToDataSync(publicKey *ecdsa.PublicKey, message []byte) error { groupID := datasync.ToOneToOneGroupID(&p.identity.PublicKey, publicKey) peerID := datasyncpeer.PublicKeyToPeerID(*publicKey) exist, err := p.datasync.IsPeerInGroup(groupID, peerID) @@ -370,7 +457,8 @@ func (p *messageProcessor) addToDataSync(publicKey *ecdsa.PublicKey, message []b // sendDataSync sends a message scheduled by the data sync layer. // Data Sync layer calls this method "dispatch" function. -func (p *messageProcessor) sendDataSync(ctx context.Context, publicKey *ecdsa.PublicKey, encodedMessage []byte, payload *datasyncproto.Payload) error { +func (p *MessageProcessor) sendDataSync(ctx context.Context, publicKey *ecdsa.PublicKey, encodedMessage []byte, payload *datasyncproto.Payload) error { + // Calculate the messageIDs messageIDs := make([][]byte, 0, len(payload.Messages)) for _, payload := range payload.Messages { messageIDs = append(messageIDs, v1protocol.MessageID(&p.identity.PublicKey, payload.Body)) @@ -381,7 +469,7 @@ func (p *messageProcessor) sendDataSync(ctx context.Context, publicKey *ecdsa.Pu return errors.Wrap(err, "failed to encrypt message") } - hash, newMessage, err := p.sendMessageSpec(ctx, publicKey, messageSpec) + hash, newMessage, err := p.sendMessageSpec(ctx, publicKey, messageSpec, messageIDs) if err != nil { return err } @@ -391,9 +479,26 @@ func (p *messageProcessor) sendDataSync(ctx context.Context, publicKey *ecdsa.Pu return nil } +// sendRawMessage sends a message not wrapped in an encryption layer +func (p *MessageProcessor) sendRawMessage(ctx context.Context, publicKey *ecdsa.PublicKey, payload []byte, messageIDs [][]byte) ([]byte, *types.NewMessage, error) { + newMessage := &types.NewMessage{ + TTL: whisperTTL, + Payload: payload, + PowTarget: calculatePoW(payload), + PowTime: whisperPoWTime, + } + + hash, err := p.transport.SendPrivateWithPartitioned(ctx, newMessage, publicKey) + if err != nil { + return nil, nil, err + } + + return hash, newMessage, nil +} + // sendMessageSpec analyses the spec properties and selects a proper transport method. -func (p *messageProcessor) sendMessageSpec(ctx context.Context, publicKey *ecdsa.PublicKey, messageSpec *encryption.ProtocolMessageSpec) ([]byte, *types.NewMessage, error) { - newMessage, err := messageSpecToWhisper(messageSpec) +func (p *MessageProcessor) sendMessageSpec(ctx context.Context, publicKey *ecdsa.PublicKey, messageSpec *encryption.ProtocolMessageSpec, messageIDs [][]byte) ([]byte, *types.NewMessage, error) { + newMessage, err := MessageSpecToWhisper(messageSpec) if err != nil { return nil, nil, err } @@ -414,10 +519,70 @@ func (p *messageProcessor) sendMessageSpec(ctx context.Context, publicKey *ecdsa return nil, nil, err } + sentMessage := &SentMessage{ + PublicKey: publicKey, + Spec: messageSpec, + MessageIDs: messageIDs, + } + + p.notifyOnSentMessage(sentMessage) + return hash, newMessage, nil } -func messageSpecToWhisper(spec *encryption.ProtocolMessageSpec) (*types.NewMessage, error) { +// SubscribeToSentMessages returns a channel where we publish every time a message is sent +func (p *MessageProcessor) SubscribeToSentMessages() <-chan *SentMessage { + c := make(chan *SentMessage, 100) + p.sentMessagesSubscriptions = append(p.sentMessagesSubscriptions, c) + return c +} + +func (p *MessageProcessor) notifyOnSentMessage(sentMessage *SentMessage) { + // Publish on channels, drop if buffer is full + for _, c := range p.sentMessagesSubscriptions { + select { + case c <- sentMessage: + default: + p.logger.Warn("sent messages subscription channel full, dropping message") + } + } + +} + +// SubscribeToScheduledMessages returns a channel where we publish every time a message is scheduled for sending +func (p *MessageProcessor) SubscribeToScheduledMessages() <-chan *RawMessage { + c := make(chan *RawMessage, 100) + p.scheduledMessagesSubscriptions = append(p.scheduledMessagesSubscriptions, c) + return c +} + +func (p *MessageProcessor) notifyOnScheduledMessage(message *RawMessage) { + // Publish on channels, drop if buffer is full + for _, c := range p.scheduledMessagesSubscriptions { + select { + case c <- message: + default: + p.logger.Warn("scheduled messages subscription channel full, dropping message") + } + } +} + +func (p *MessageProcessor) JoinPublic(chatID string) error { + return p.transport.JoinPublic(chatID) +} + +// AddEphemeralKey adds an ephemeral key that we will be listening to +// note that we never removed them from now, as waku/whisper does not +// recalculate topics on removal, so effectively there's no benefit. +// On restart they will be gone. +func (p *MessageProcessor) AddEphemeralKey(privateKey *ecdsa.PrivateKey) (*transport.Filter, error) { + p.ephemeralKeysMutex.Lock() + p.ephemeralKeys[types.EncodeHex(crypto.FromECDSAPub(&privateKey.PublicKey))] = privateKey + p.ephemeralKeysMutex.Unlock() + return p.transport.LoadKeyFilters(privateKey) +} + +func MessageSpecToWhisper(spec *encryption.ProtocolMessageSpec) (*types.NewMessage, error) { var newMessage *types.NewMessage payload, err := proto.Marshal(spec.Message) @@ -444,9 +609,3 @@ func calculatePoW(payload []byte) float64 { } return whisperDefaultPoW } - -// isPubKeyEqual checks that two public keys are equal -func isPubKeyEqual(a, b *ecdsa.PublicKey) bool { - // the curve is always the same, just compare the points - return a.X.Cmp(b.X) == 0 && a.Y.Cmp(b.Y) == 0 -} diff --git a/protocol/message_processor_test.go b/protocol/common/message_processor_test.go similarity index 88% rename from protocol/message_processor_test.go rename to protocol/common/message_processor_test.go index 6851e04d4..9cabbf935 100644 --- a/protocol/message_processor_test.go +++ b/protocol/common/message_processor_test.go @@ -1,4 +1,4 @@ -package protocol +package common import ( "io/ioutil" @@ -33,22 +33,20 @@ func TestMessageProcessorSuite(t *testing.T) { type MessageProcessorSuite struct { suite.Suite - processor *messageProcessor + processor *MessageProcessor tmpDir string - testMessage Message + testMessage protobuf.ChatMessage logger *zap.Logger } func (s *MessageProcessorSuite) SetupTest() { - s.testMessage = Message{ - ChatMessage: protobuf.ChatMessage{ - Text: "abc123", - ChatId: "testing-adamb", - ContentType: protobuf.ChatMessage_TEXT_PLAIN, - MessageType: protobuf.ChatMessage_PUBLIC_GROUP, - Clock: 154593077368201, - Timestamp: 1545930773682, - }, + s.testMessage = protobuf.ChatMessage{ + Text: "abc123", + ChatId: "testing-adamb", + ContentType: protobuf.ChatMessage_TEXT_PLAIN, + MessageType: protobuf.ChatMessage_PUBLIC_GROUP, + Clock: 154593077368201, + Timestamp: 1545930773682, } var err error @@ -81,8 +79,6 @@ func (s *MessageProcessorSuite) SetupTest() { whisperConfig.MinimumAcceptedPOW = 0 shh := whisper.New(&whisperConfig) s.Require().NoError(shh.Start(nil)) - config := &config{} - s.Require().NoError(WithDatasync()(config)) whisperTransport, err := transport.NewTransport( gethbridge.NewGethWhisperWrapper(shh), @@ -94,13 +90,13 @@ func (s *MessageProcessorSuite) SetupTest() { ) s.Require().NoError(err) - s.processor, err = newMessageProcessor( + s.processor, err = NewMessageProcessor( identity, database, encryptionProtocol, whisperTransport, s.logger, - featureFlags{}, + FeatureFlags{}, ) s.Require().NoError(err) } @@ -127,7 +123,7 @@ func (s *MessageProcessorSuite) TestHandleDecodedMessagesWrapped() { message.Sig = crypto.FromECDSAPub(&relayerKey.PublicKey) message.Payload = wrappedPayload - decodedMessages, err := s.processor.handleMessages(message, true) + decodedMessages, err := s.processor.HandleMessages(message, true) s.Require().NoError(err) s.Require().Equal(1, len(decodedMessages)) @@ -135,7 +131,7 @@ func (s *MessageProcessorSuite) TestHandleDecodedMessagesWrapped() { s.Require().Equal(v1protocol.MessageID(&authorKey.PublicKey, wrappedPayload), decodedMessages[0].ID) parsedMessage := decodedMessages[0].ParsedMessage.(protobuf.ChatMessage) s.Require().Equal(encodedPayload, decodedMessages[0].DecryptedPayload) - s.Require().True(proto.Equal(&s.testMessage.ChatMessage, &parsedMessage)) + s.Require().True(proto.Equal(&s.testMessage, &parsedMessage)) s.Require().Equal(protobuf.ApplicationMetadataMessage_CHAT_MESSAGE, decodedMessages[0].Type) } @@ -163,7 +159,7 @@ func (s *MessageProcessorSuite) TestHandleDecodedMessagesDatasync() { message.Sig = crypto.FromECDSAPub(&relayerKey.PublicKey) message.Payload = marshalledDataSyncMessage - decodedMessages, err := s.processor.handleMessages(message, true) + decodedMessages, err := s.processor.HandleMessages(message, true) s.Require().NoError(err) // We send two messages, the unwrapped one will be attributed to the relayer, while the wrapped one will be attributed to the author @@ -172,7 +168,7 @@ func (s *MessageProcessorSuite) TestHandleDecodedMessagesDatasync() { s.Require().Equal(v1protocol.MessageID(&authorKey.PublicKey, wrappedPayload), decodedMessages[0].ID) s.Require().Equal(encodedPayload, decodedMessages[0].DecryptedPayload) parsedMessage := decodedMessages[0].ParsedMessage.(protobuf.ChatMessage) - s.Require().True(proto.Equal(&s.testMessage.ChatMessage, &parsedMessage)) + s.Require().True(proto.Equal(&s.testMessage, &parsedMessage)) s.Require().Equal(protobuf.ApplicationMetadataMessage_CHAT_MESSAGE, decodedMessages[0].Type) } @@ -230,7 +226,7 @@ func (s *MessageProcessorSuite) TestHandleDecodedMessagesDatasyncEncrypted() { message.Sig = crypto.FromECDSAPub(&relayerKey.PublicKey) message.Payload = encryptedPayload - decodedMessages, err := s.processor.handleMessages(message, true) + decodedMessages, err := s.processor.HandleMessages(message, true) s.Require().NoError(err) // We send two messages, the unwrapped one will be attributed to the relayer, @@ -240,6 +236,6 @@ func (s *MessageProcessorSuite) TestHandleDecodedMessagesDatasyncEncrypted() { s.Require().Equal(v1protocol.MessageID(&authorKey.PublicKey, wrappedPayload), decodedMessages[0].ID) s.Require().Equal(encodedPayload, decodedMessages[0].DecryptedPayload) parsedMessage := decodedMessages[0].ParsedMessage.(protobuf.ChatMessage) - s.Require().True(proto.Equal(&s.testMessage.ChatMessage, &parsedMessage)) + s.Require().True(proto.Equal(&s.testMessage, &parsedMessage)) s.Require().Equal(protobuf.ApplicationMetadataMessage_CHAT_MESSAGE, decodedMessages[0].Type) } diff --git a/protocol/common/raw_message.go b/protocol/common/raw_message.go new file mode 100644 index 000000000..9ac87cebe --- /dev/null +++ b/protocol/common/raw_message.go @@ -0,0 +1,24 @@ +package common + +import ( + "crypto/ecdsa" + + "github.com/status-im/status-go/protocol/protobuf" +) + +// RawMessage represent a sent or received message, kept for being able +// to re-send/propagate +type RawMessage struct { + ID string + LocalChatID string + LastSent uint64 + SendCount int + Sent bool + ResendAutomatically bool + SkipEncryption bool + SendPushNotification bool + MessageType protobuf.ApplicationMetadataMessage_Type + Payload []byte + Sender *ecdsa.PrivateKey + Recipients []*ecdsa.PublicKey +} diff --git a/protocol/message.go b/protocol/message.go index 2649033eb..27c6e885b 100644 --- a/protocol/message.go +++ b/protocol/message.go @@ -109,20 +109,6 @@ type Message struct { SigPubKey *ecdsa.PublicKey `json:"-"` } -// RawMessage represent a sent or received message, kept for being able -// to re-send/propagate -type RawMessage struct { - ID string - LocalChatID string - LastSent uint64 - SendCount int - Sent bool - ResendAutomatically bool - MessageType protobuf.ApplicationMetadataMessage_Type - Payload []byte - Recipients []*ecdsa.PublicKey -} - func (m *Message) MarshalJSON() ([]byte, error) { type StickerAlias struct { Hash string `json:"hash"` diff --git a/protocol/message_handler.go b/protocol/message_handler.go index 161b3f557..5713378a8 100644 --- a/protocol/message_handler.go +++ b/protocol/message_handler.go @@ -9,6 +9,7 @@ import ( "go.uber.org/zap" "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/encryption/multidevice" "github.com/status-im/status-go/protocol/protobuf" v1protocol "github.com/status-im/status-go/protocol/v1" @@ -146,7 +147,7 @@ func (m *MessageHandler) handleCommandMessage(state *ReceivedMessageState, messa message.LocalChatID = chat.ID // Increase unviewed count - if !isPubKeyEqual(message.SigPubKey, &m.identity.PublicKey) { + if !common.IsPubKeyEqual(message.SigPubKey, &m.identity.PublicKey) { chat.UnviewedMessagesCount++ message.OutgoingStatus = "" } else { @@ -332,7 +333,7 @@ func (m *MessageHandler) HandleChatMessage(state *ReceivedMessageState) error { receivedMessage.LocalChatID = chat.ID // Increase unviewed count - if !isPubKeyEqual(receivedMessage.SigPubKey, &m.identity.PublicKey) { + if !common.IsPubKeyEqual(receivedMessage.SigPubKey, &m.identity.PublicKey) { chat.UnviewedMessagesCount++ } else { // Our own message, mark as sent @@ -582,7 +583,7 @@ func (m *MessageHandler) matchMessage(message *Message, chats map[string]*Chat, return nil, errors.New("received a public message from non-existing chat") } return chat, nil - case message.MessageType == protobuf.ChatMessage_ONE_TO_ONE && isPubKeyEqual(message.SigPubKey, &m.identity.PublicKey): + case message.MessageType == protobuf.ChatMessage_ONE_TO_ONE && common.IsPubKeyEqual(message.SigPubKey, &m.identity.PublicKey): // It's a private message coming from us so we rely on Message.ChatID // If chat does not exist, it should be created to support multidevice synchronization. chatID := message.ChatId diff --git a/protocol/messenger.go b/protocol/messenger.go index 092d18013..1b1bb70d5 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "math/rand" "os" + "reflect" "sync" "time" @@ -18,6 +19,7 @@ import ( "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" enstypes "github.com/status-im/status-go/eth-node/types/ens" + "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/encryption" "github.com/status-im/status-go/protocol/encryption/multidevice" "github.com/status-im/status-go/protocol/encryption/sharedsecret" @@ -25,6 +27,8 @@ import ( "github.com/status-im/status-go/protocol/identity/identicon" "github.com/status-im/status-go/protocol/images" "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/pushnotificationclient" + "github.com/status-im/status-go/protocol/pushnotificationserver" "github.com/status-im/status-go/protocol/sqlite" "github.com/status-im/status-go/protocol/transport" wakutransp "github.com/status-im/status-go/protocol/transport/waku" @@ -55,11 +59,13 @@ type Messenger struct { persistence *sqlitePersistence transport transport.Transport encryptor *encryption.Protocol - processor *messageProcessor + processor *common.MessageProcessor handler *MessageHandler + pushNotificationClient *pushnotificationclient.Client + pushNotificationServer *pushnotificationserver.Server logger *zap.Logger verifyTransactionClient EthClient - featureFlags featureFlags + featureFlags common.FeatureFlags messagesPersistenceEnabled bool shutdownTasks []func() error systemMessagesTranslations map[protobuf.MembershipUpdateEvent_EventType]string @@ -68,6 +74,8 @@ type Messenger struct { allInstallations map[string]*multidevice.Installation modifiedInstallations map[string]bool installationID string + mailserver []byte + database *sql.DB mutex sync.Mutex } @@ -88,111 +96,11 @@ func (m *MessengerResponse) IsEmpty() bool { return len(m.Chats) == 0 && len(m.Messages) == 0 && len(m.Contacts) == 0 && len(m.Installations) == 0 } -type featureFlags struct { - // datasync indicates whether direct messages should be sent exclusively - // using datasync, breaking change for non-v1 clients. Public messages - // are not impacted - datasync bool -} - type dbConfig struct { dbPath string dbKey string } -type config struct { - // This needs to be exposed until we move here mailserver logic - // as otherwise the client is not notified of a new filter and - // won't be pulling messages from mailservers until it reloads the chats/filters - onNegotiatedFilters func([]*transport.Filter) - // DEPRECATED: no need to expose it - onSendContactCodeHandler func(*encryption.ProtocolMessageSpec) - - // systemMessagesTranslations holds translations for system-messages - systemMessagesTranslations map[protobuf.MembershipUpdateEvent_EventType]string - // Config for the envelopes monitor - envelopesMonitorConfig *transport.EnvelopesMonitorConfig - - messagesPersistenceEnabled bool - featureFlags featureFlags - - // A path to a database or a database instance is required. - // The database instance has a higher priority. - dbConfig dbConfig - db *sql.DB - - verifyTransactionClient EthClient - - logger *zap.Logger -} - -type Option func(*config) error - -// WithSystemMessagesTranslations is required for Group Chats which are currently disabled. -// nolint: unused -func WithSystemMessagesTranslations(t map[protobuf.MembershipUpdateEvent_EventType]string) Option { - return func(c *config) error { - c.systemMessagesTranslations = t - return nil - } -} - -func WithOnNegotiatedFilters(h func([]*transport.Filter)) Option { - return func(c *config) error { - c.onNegotiatedFilters = h - return nil - } -} - -func WithCustomLogger(logger *zap.Logger) Option { - return func(c *config) error { - c.logger = logger - return nil - } -} - -func WithMessagesPersistenceEnabled() Option { - return func(c *config) error { - c.messagesPersistenceEnabled = true - return nil - } -} - -func WithDatabaseConfig(dbPath, dbKey string) Option { - return func(c *config) error { - c.dbConfig = dbConfig{dbPath: dbPath, dbKey: dbKey} - return nil - } -} - -func WithVerifyTransactionClient(client EthClient) Option { - return func(c *config) error { - c.verifyTransactionClient = client - return nil - } -} - -func WithDatabase(db *sql.DB) Option { - return func(c *config) error { - c.db = db - return nil - } -} - -func WithDatasync() func(c *config) error { - return func(c *config) error { - c.featureFlags.datasync = true - return nil - } -} - -func WithEnvelopesMonitorConfig(emc *transport.EnvelopesMonitorConfig) Option { - return func(c *config) error { - c.envelopesMonitorConfig = emc - return nil - } -} - func NewMessenger( identity *ecdsa.PrivateKey, node types.Node, @@ -245,7 +153,7 @@ func NewMessenger( slogger := logger.With(zap.String("site", "onSendContactCodeHandler")) slogger.Debug("received a SendContactCode request") - newMessage, err := messageSpecToWhisper(messageSpec) + newMessage, err := common.MessageSpecToWhisper(messageSpec) if err != nil { slogger.Warn("failed to convert spec to Whisper message", zap.Error(err)) return @@ -328,7 +236,7 @@ func NewMessenger( logger, ) - processor, err := newMessageProcessor( + processor, err := common.NewMessageProcessor( identity, database, encryptionProtocol, @@ -340,6 +248,30 @@ func NewMessenger( return nil, errors.Wrap(err, "failed to create messageProcessor") } + // Initialize push notification server + var pushNotificationServer *pushnotificationserver.Server + if c.pushNotificationServerConfig != nil { + c.pushNotificationServerConfig.Identity = identity + pushNotificationServerPersistence := pushnotificationserver.NewSQLitePersistence(database) + pushNotificationServer = pushnotificationserver.New(c.pushNotificationServerConfig, pushNotificationServerPersistence, processor) + } + + // Initialize push notification client + pushNotificationClientPersistence := pushnotificationclient.NewPersistence(database) + pushNotificationClientConfig := c.pushNotificationClientConfig + if pushNotificationClientConfig == nil { + pushNotificationClientConfig = &pushnotificationclient.Config{} + } + + // Overriding until we handle different identities + pushNotificationClientConfig.Identity = identity + // Hardcoding this for now, as it's the only one we support + pushNotificationClientConfig.TokenType = protobuf.PushNotificationRegistration_APN_TOKEN + pushNotificationClientConfig.Logger = logger + pushNotificationClientConfig.InstallationID = installationID + + pushNotificationClient := pushnotificationclient.New(pushNotificationClientPersistence, pushNotificationClientConfig, processor) + handler := newMessageHandler(identity, logger, &sqlitePersistence{db: database}) messenger = &Messenger{ @@ -350,6 +282,8 @@ func NewMessenger( encryptor: encryptionProtocol, processor: processor, handler: handler, + pushNotificationClient: pushNotificationClient, + pushNotificationServer: pushNotificationServer, featureFlags: c.featureFlags, systemMessagesTranslations: c.systemMessagesTranslations, allChats: make(map[string]*Chat), @@ -359,8 +293,10 @@ func NewMessenger( modifiedInstallations: make(map[string]bool), messagesPersistenceEnabled: c.messagesPersistenceEnabled, verifyTransactionClient: c.verifyTransactionClient, + database: database, shutdownTasks: []func() error{ database.Close, + pushNotificationClient.Stop, transp.ResetFilters, transp.Stop, func() error { processor.Stop(); return nil }, @@ -377,6 +313,21 @@ func NewMessenger( } func (m *Messenger) Start() error { + m.logger.Info("starting messenger", zap.String("identity", types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)))) + // Start push notification server + if m.pushNotificationServer != nil { + if err := m.pushNotificationServer.Start(); err != nil { + return err + } + } + + // Start push notification client + if m.pushNotificationClient != nil { + if err := m.pushNotificationClient.Start(); err != nil { + return err + } + } + return m.encryptor.Start(m.identity) } @@ -674,7 +625,7 @@ func (m *Messenger) CreateGroupChatWithMembers(ctx context.Context, name string, } m.allChats[chat.ID] = &chat - _, err = m.dispatchMessage(ctx, &RawMessage{ + _, err = m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE, @@ -740,7 +691,7 @@ func (m *Messenger) RemoveMemberFromGroupChat(ctx context.Context, chatID string if err != nil { return nil, err } - _, err = m.dispatchMessage(ctx, &RawMessage{ + _, err = m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE, @@ -803,7 +754,7 @@ func (m *Messenger) AddMembersToGroupChat(ctx context.Context, chatID string, me if err != nil { return nil, err } - _, err = m.dispatchMessage(ctx, &RawMessage{ + _, err = m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE, @@ -868,7 +819,7 @@ func (m *Messenger) ChangeGroupChatName(ctx context.Context, chatID string, name if err != nil { return nil, err } - _, err = m.dispatchMessage(ctx, &RawMessage{ + _, err = m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE, @@ -934,7 +885,7 @@ func (m *Messenger) AddAdminsToGroupChat(ctx context.Context, chatID string, mem if err != nil { return nil, err } - _, err = m.dispatchMessage(ctx, &RawMessage{ + _, err = m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE, @@ -1002,7 +953,7 @@ func (m *Messenger) ConfirmJoiningGroup(ctx context.Context, chatID string) (*Me if err != nil { return nil, err } - _, err = m.dispatchMessage(ctx, &RawMessage{ + _, err = m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE, @@ -1070,7 +1021,7 @@ func (m *Messenger) LeaveGroupChat(ctx context.Context, chatID string, remove bo if err != nil { return nil, err } - _, err = m.dispatchMessage(ctx, &RawMessage{ + _, err = m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE, @@ -1174,6 +1125,14 @@ func (m *Messenger) isNewContact(contact *Contact) bool { return contact.IsAdded() && (!ok || !previousContact.IsAdded()) } +func (m *Messenger) removedContact(contact *Contact) bool { + previousContact, ok := m.allContacts[contact.ID] + if !ok { + return false + } + return previousContact.IsAdded() && !contact.IsAdded() +} + func (m *Messenger) saveContact(contact *Contact) error { name, identicon, err := generateAliasAndIdenticon(contact.ID) if err != nil { @@ -1190,6 +1149,9 @@ func (m *Messenger) saveContact(contact *Contact) error { } } + // We check if it should re-register with the push notification server + shouldReregisterForPushNotifications := m.pushNotificationClient != nil && (m.isNewContact(contact) || m.removedContact(contact)) + err = m.persistence.SaveContact(contact, nil) if err != nil { return err @@ -1197,6 +1159,16 @@ func (m *Messenger) saveContact(contact *Contact) error { m.allContacts[contact.ID] = contact + // Reregister only when data has changed + if shouldReregisterForPushNotifications { + m.logger.Info("contact state changed, re-registering for push notification") + contactIDs, mutedChatIDs := m.addedContactsAndMutedChatIDs() + err := m.pushNotificationClient.Reregister(contactIDs, mutedChatIDs) + if err != nil { + return err + } + } + return nil } @@ -1256,11 +1228,12 @@ func (m *Messenger) ReSendChatMessage(ctx context.Context, messageID string) err return errors.New("chat not found") } - _, err = m.dispatchMessage(ctx, &RawMessage{ - LocalChatID: chat.ID, - Payload: message.Payload, - MessageType: message.MessageType, - Recipients: message.Recipients, + _, err = m.dispatchMessage(ctx, &common.RawMessage{ + LocalChatID: chat.ID, + Payload: message.Payload, + MessageType: message.MessageType, + Recipients: message.Recipients, + ResendAutomatically: message.ResendAutomatically, }) return err } @@ -1276,7 +1249,7 @@ func (m *Messenger) hasPairedDevices() bool { } // sendToPairedDevices will check if we have any paired devices and send to them if necessary -func (m *Messenger) sendToPairedDevices(ctx context.Context, spec *RawMessage) error { +func (m *Messenger) sendToPairedDevices(ctx context.Context, spec *common.RawMessage) error { hasPairedDevices := m.hasPairedDevices() // We send a message to any paired device if hasPairedDevices { @@ -1288,7 +1261,7 @@ func (m *Messenger) sendToPairedDevices(ctx context.Context, spec *RawMessage) e return nil } -func (m *Messenger) dispatchPairInstallationMessage(ctx context.Context, spec *RawMessage) ([]byte, error) { +func (m *Messenger) dispatchPairInstallationMessage(ctx context.Context, spec *common.RawMessage) ([]byte, error) { var err error var id []byte @@ -1307,7 +1280,7 @@ func (m *Messenger) dispatchPairInstallationMessage(ctx context.Context, spec *R return id, nil } -func (m *Messenger) dispatchMessage(ctx context.Context, spec *RawMessage) ([]byte, error) { +func (m *Messenger) dispatchMessage(ctx context.Context, spec *common.RawMessage) ([]byte, error) { var err error var id []byte logger := m.logger.With(zap.String("site", "dispatchMessage"), zap.String("chatID", spec.LocalChatID)) @@ -1322,7 +1295,7 @@ func (m *Messenger) dispatchMessage(ctx context.Context, spec *RawMessage) ([]by if err != nil { return nil, err } - if !isPubKeyEqual(publicKey, &m.identity.PublicKey) { + if !common.IsPubKeyEqual(publicKey, &m.identity.PublicKey) { id, err = m.processor.SendPrivate(ctx, publicKey, spec) if err != nil { @@ -1357,7 +1330,7 @@ func (m *Messenger) dispatchMessage(ctx context.Context, spec *RawMessage) ([]by // Filter out my key from the recipients n := 0 for _, recipient := range spec.Recipients { - if !isPubKeyEqual(recipient, &m.identity.PublicKey) { + if !common.IsPubKeyEqual(recipient, &m.identity.PublicKey) { spec.Recipients[n] = recipient n++ } @@ -1457,10 +1430,12 @@ func (m *Messenger) SendChatMessage(ctx context.Context, message *Message) (*Mes return nil, errors.New("chat type not supported") } - id, err := m.dispatchMessage(ctx, &RawMessage{ - LocalChatID: chat.ID, - Payload: encodedMessage, - MessageType: protobuf.ApplicationMetadataMessage_CHAT_MESSAGE, + id, err := m.dispatchMessage(ctx, &common.RawMessage{ + LocalChatID: chat.ID, + SendPushNotification: !chat.Public(), + Payload: encodedMessage, + MessageType: protobuf.ApplicationMetadataMessage_CHAT_MESSAGE, + ResendAutomatically: true, }) if err != nil { return nil, err @@ -1564,7 +1539,7 @@ func (m *Messenger) sendContactUpdate(ctx context.Context, chatID, ensName, prof return nil, err } - _, err = m.dispatchMessage(ctx, &RawMessage{ + _, err = m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chatID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_CONTACT_UPDATE, @@ -1658,7 +1633,7 @@ func (m *Messenger) SendPairInstallation(ctx context.Context) (*MessengerRespons return nil, err } - _, err = m.dispatchPairInstallationMessage(ctx, &RawMessage{ + _, err = m.dispatchPairInstallationMessage(ctx, &common.RawMessage{ LocalChatID: chatID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_PAIR_INSTALLATION, @@ -1705,7 +1680,7 @@ func (m *Messenger) syncPublicChat(ctx context.Context, publicChat *Chat) error return err } - _, err = m.dispatchMessage(ctx, &RawMessage{ + _, err = m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chatID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_PUBLIC_CHAT, @@ -1748,7 +1723,7 @@ func (m *Messenger) syncContact(ctx context.Context, contact *Contact) error { return err } - _, err = m.dispatchMessage(ctx, &RawMessage{ + _, err = m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chatID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SYNC_INSTALLATION_CONTACT, @@ -1828,7 +1803,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte for _, messages := range chatWithMessages { for _, shhMessage := range messages { // TODO: fix this to use an exported method. - statusMessages, err := m.processor.handleMessages(shhMessage, true) + statusMessages, err := m.processor.HandleMessages(shhMessage, true) if err != nil { logger.Info("failed to decode messages", zap.Error(err)) continue @@ -1897,7 +1872,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte continue } case protobuf.PairInstallation: - if !isPubKeyEqual(messageState.CurrentMessageState.PublicKey, &m.identity.PublicKey) { + if !common.IsPubKeyEqual(messageState.CurrentMessageState.PublicKey, &m.identity.PublicKey) { logger.Warn("not coming from us, ignoring") continue } @@ -1910,7 +1885,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte } case protobuf.SyncInstallationContact: - if !isPubKeyEqual(messageState.CurrentMessageState.PublicKey, &m.identity.PublicKey) { + if !common.IsPubKeyEqual(messageState.CurrentMessageState.PublicKey, &m.identity.PublicKey) { logger.Warn("not coming from us, ignoring") continue } @@ -1923,7 +1898,7 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte continue } case protobuf.SyncInstallationPublicChat: - if !isPubKeyEqual(messageState.CurrentMessageState.PublicKey, &m.identity.PublicKey) { + if !common.IsPubKeyEqual(messageState.CurrentMessageState.PublicKey, &m.identity.PublicKey) { logger.Warn("not coming from us, ignoring") continue } @@ -1996,8 +1971,80 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte logger.Warn("failed to handle ContactUpdate", zap.Error(err)) continue } + case protobuf.PushNotificationQuery: + logger.Debug("Received PushNotificationQuery") + if m.pushNotificationServer == nil { + continue + } + logger.Debug("Handling PushNotificationQuery") + if err := m.pushNotificationServer.HandlePushNotificationQuery(publicKey, msg.ID, msg.ParsedMessage.(protobuf.PushNotificationQuery)); err != nil { + logger.Warn("failed to handle PushNotificationQuery", zap.Error(err)) + } + // We continue in any case, no changes to messenger + continue + case protobuf.PushNotificationRegistrationResponse: + logger.Debug("Received PushNotificationRegistrationResponse") + if m.pushNotificationClient == nil { + continue + } + logger.Debug("Handling PushNotificationRegistrationResponse") + if err := m.pushNotificationClient.HandlePushNotificationRegistrationResponse(publicKey, msg.ParsedMessage.(protobuf.PushNotificationRegistrationResponse)); err != nil { + logger.Warn("failed to handle PushNotificationRegistrationResponse", zap.Error(err)) + } + // We continue in any case, no changes to messenger + continue + case protobuf.PushNotificationResponse: + logger.Debug("Received PushNotificationResponse") + if m.pushNotificationClient == nil { + continue + } + logger.Debug("Handling PushNotificationResponse") + if err := m.pushNotificationClient.HandlePushNotificationResponse(publicKey, msg.ParsedMessage.(protobuf.PushNotificationResponse)); err != nil { + logger.Warn("failed to handle PushNotificationResponse", zap.Error(err)) + } + // We continue in any case, no changes to messenger + continue + + case protobuf.PushNotificationQueryResponse: + logger.Debug("Received PushNotificationQueryResponse") + if m.pushNotificationClient == nil { + continue + } + logger.Debug("Handling PushNotificationQueryResponse") + if err := m.pushNotificationClient.HandlePushNotificationQueryResponse(publicKey, msg.ParsedMessage.(protobuf.PushNotificationQueryResponse)); err != nil { + logger.Warn("failed to handle PushNotificationQueryResponse", zap.Error(err)) + } + // We continue in any case, no changes to messenger + continue + + case protobuf.PushNotificationRequest: + logger.Debug("Received PushNotificationRequest") + if m.pushNotificationServer == nil { + continue + } + logger.Debug("Handling PushNotificationRequest") + if err := m.pushNotificationServer.HandlePushNotificationRequest(publicKey, msg.ParsedMessage.(protobuf.PushNotificationRequest)); err != nil { + logger.Warn("failed to handle PushNotificationRequest", zap.Error(err)) + } + // We continue in any case, no changes to messenger + continue + default: - logger.Debug("message not handled") + // Check if is an encrypted PushNotificationRegistration + if msg.Type == protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION { + logger.Debug("Received PushNotificationRegistration") + if m.pushNotificationServer == nil { + continue + } + logger.Debug("Handling PushNotificationRegistration") + if err := m.pushNotificationServer.HandlePushNotificationRegistration(publicKey, msg.ParsedMessage.([]byte)); err != nil { + logger.Warn("failed to handle PushNotificationRegistration", zap.Error(err)) + } + // We continue in any case, no changes to messenger + continue + } + + logger.Debug("message not handled", zap.Any("messageType", reflect.TypeOf(msg.ParsedMessage))) } } @@ -2076,13 +2123,20 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte return messageState.Response, nil } +// SetMailserver sets the currently used mailserver +func (m *Messenger) SetMailserver(peer []byte) { + m.mailserver = peer +} + func (m *Messenger) RequestHistoricMessages( ctx context.Context, - peer []byte, // should be removed after mailserver logic is ported from, to uint32, cursor []byte, ) ([]byte, error) { - return m.transport.SendMessagesRequest(ctx, peer, from, to, cursor) + if m.mailserver == nil { + return nil, errors.New("no mailserver selected") + } + return m.transport.SendMessagesRequest(ctx, m.mailserver, from, to, cursor) } func (m *Messenger) LoadFilters(filters []*transport.Filter) ([]*transport.Filter, error) { @@ -2163,6 +2217,46 @@ func (m *Messenger) MarkAllRead(chatID string) error { return nil } +// MuteChat signals to the messenger that we don't want to be notified +// on new messages from this chat +func (m *Messenger) MuteChat(chatID string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + chat, ok := m.allChats[chatID] + if !ok { + return errors.New("chat not found") + } + + err := m.persistence.MuteChat(chatID) + if err != nil { + return err + } + + chat.Muted = true + m.allChats[chat.ID] = chat + return nil +} + +// UnmuteChat signals to the messenger that we want to be notified +// on new messages from this chat +func (m *Messenger) UnmuteChat(chatID string) error { + m.mutex.Lock() + defer m.mutex.Unlock() + chat, ok := m.allChats[chatID] + if !ok { + return errors.New("chat not found") + } + + err := m.persistence.UnmuteChat(chatID) + if err != nil { + return err + } + + chat.Muted = false + m.allChats[chat.ID] = chat + return nil +} + func (m *Messenger) UpdateMessageOutgoingStatus(id, newOutgoingStatus string) error { return m.persistence.UpdateMessageOutgoingStatus(id, newOutgoingStatus) } @@ -2274,7 +2368,7 @@ func (m *Messenger) RequestTransaction(ctx context.Context, chatID, value, contr if err != nil { return nil, err } - id, err := m.dispatchMessage(ctx, &RawMessage{ + id, err := m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_REQUEST_TRANSACTION, @@ -2350,7 +2444,7 @@ func (m *Messenger) RequestAddressForTransaction(ctx context.Context, chatID, fr if err != nil { return nil, err } - id, err := m.dispatchMessage(ctx, &RawMessage{ + id, err := m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_REQUEST_ADDRESS_FOR_TRANSACTION, @@ -2452,7 +2546,7 @@ func (m *Messenger) AcceptRequestAddressForTransaction(ctx context.Context, mess return nil, err } - newMessageID, err := m.dispatchMessage(ctx, &RawMessage{ + newMessageID, err := m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_ACCEPT_REQUEST_ADDRESS_FOR_TRANSACTION, @@ -2535,7 +2629,7 @@ func (m *Messenger) DeclineRequestTransaction(ctx context.Context, messageID str return nil, err } - newMessageID, err := m.dispatchMessage(ctx, &RawMessage{ + newMessageID, err := m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_DECLINE_REQUEST_TRANSACTION, @@ -2617,7 +2711,7 @@ func (m *Messenger) DeclineRequestAddressForTransaction(ctx context.Context, mes return nil, err } - newMessageID, err := m.dispatchMessage(ctx, &RawMessage{ + newMessageID, err := m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_DECLINE_REQUEST_ADDRESS_FOR_TRANSACTION, @@ -2714,7 +2808,7 @@ func (m *Messenger) AcceptRequestTransaction(ctx context.Context, transactionHas return nil, err } - newMessageID, err := m.dispatchMessage(ctx, &RawMessage{ + newMessageID, err := m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SEND_TRANSACTION, @@ -2791,7 +2885,7 @@ func (m *Messenger) SendTransaction(ctx context.Context, chatID, value, contract return nil, err } - newMessageID, err := m.dispatchMessage(ctx, &RawMessage{ + newMessageID, err := m.dispatchMessage(ctx, &common.RawMessage{ LocalChatID: chat.ID, Payload: encodedMessage, MessageType: protobuf.ApplicationMetadataMessage_SEND_TRANSACTION, @@ -2965,6 +3059,142 @@ func (m *Messenger) Timesource() TimeSource { return m.getTimesource() } +// AddPushNotificationsServer adds a push notification server +func (m *Messenger) AddPushNotificationsServer(ctx context.Context, publicKey *ecdsa.PublicKey) error { + if m.pushNotificationClient == nil { + return errors.New("push notification client not enabled") + } + return m.pushNotificationClient.AddPushNotificationsServer(publicKey) +} + +// RemovePushNotificationServer removes a push notification server +func (m *Messenger) RemovePushNotificationServer(ctx context.Context, publicKey *ecdsa.PublicKey) error { + if m.pushNotificationClient == nil { + return errors.New("push notification client not enabled") + } + return m.pushNotificationClient.RemovePushNotificationServer(publicKey) +} + +// UnregisterFromPushNotifications unregister from any server +func (m *Messenger) UnregisterFromPushNotifications(ctx context.Context) error { + return m.pushNotificationClient.Unregister() +} + +// DisableSendingPushNotifications signals the client not to send any push notification +func (m *Messenger) DisableSendingPushNotifications() error { + if m.pushNotificationClient == nil { + return errors.New("push notification client not enabled") + } + m.pushNotificationClient.DisableSending() + return nil +} + +// EnableSendingPushNotifications signals the client to send push notifications +func (m *Messenger) EnableSendingPushNotifications() error { + if m.pushNotificationClient == nil { + return errors.New("push notification client not enabled") + } + m.pushNotificationClient.EnableSending() + return nil +} + +func (m *Messenger) addedContactsAndMutedChatIDs() ([]*ecdsa.PublicKey, []string) { + var contactIDs []*ecdsa.PublicKey + var mutedChatIDs []string + + for _, contact := range m.allContacts { + if contact.IsAdded() { + pk, err := contact.PublicKey() + if err != nil { + m.logger.Warn("could not parse contact public key") + continue + } + contactIDs = append(contactIDs, pk) + } else if contact.IsBlocked() { + mutedChatIDs = append(mutedChatIDs, contact.ID) + } + } + for _, chat := range m.allChats { + if chat.Muted { + mutedChatIDs = append(mutedChatIDs, chat.ID) + } + + } + return contactIDs, mutedChatIDs +} + +// RegisterForPushNotification register deviceToken with any push notification server enabled +func (m *Messenger) RegisterForPushNotifications(ctx context.Context, deviceToken string) error { + if m.pushNotificationClient == nil { + return errors.New("push notification client not enabled") + } + m.mutex.Lock() + defer m.mutex.Unlock() + + contactIDs, mutedChatIDs := m.addedContactsAndMutedChatIDs() + return m.pushNotificationClient.Register(deviceToken, contactIDs, mutedChatIDs) +} + +// RegisteredForPushNotifications returns whether we successfully registered with all the servers +func (m *Messenger) RegisteredForPushNotifications() (bool, error) { + if m.pushNotificationClient == nil { + return false, errors.New("no push notification client") + } + return m.pushNotificationClient.Registered() +} + +// EnablePushNotificationsFromContactsOnly is used to indicate that we want to received push notifications only from contacts +func (m *Messenger) EnablePushNotificationsFromContactsOnly() error { + if m.pushNotificationClient == nil { + return errors.New("no push notification client") + } + m.mutex.Lock() + defer m.mutex.Unlock() + + contactIDs, mutedChatIDs := m.addedContactsAndMutedChatIDs() + return m.pushNotificationClient.EnablePushNotificationsFromContactsOnly(contactIDs, mutedChatIDs) +} + +// DisablePushNotificationsFromContactsOnly is used to indicate that we want to received push notifications from anyone +func (m *Messenger) DisablePushNotificationsFromContactsOnly() error { + if m.pushNotificationClient == nil { + return errors.New("no push notification client") + } + m.mutex.Lock() + defer m.mutex.Unlock() + + contactIDs, mutedChatIDs := m.addedContactsAndMutedChatIDs() + return m.pushNotificationClient.DisablePushNotificationsFromContactsOnly(contactIDs, mutedChatIDs) +} + +// GetPushNotificationServers returns the servers used for push notifications +func (m *Messenger) GetPushNotificationServers() ([]*pushnotificationclient.PushNotificationServer, error) { + if m.pushNotificationClient == nil { + return nil, errors.New("no push notification client") + } + return m.pushNotificationClient.GetServers() +} + +// StartPushNotificationsServer initialize and start a push notification server, using the current messenger identity key +func (m *Messenger) StartPushNotificationsServer() error { + if m.pushNotificationServer == nil { + pushNotificationServerPersistence := pushnotificationserver.NewSQLitePersistence(m.database) + config := &pushnotificationserver.Config{ + Logger: m.logger, + Identity: m.identity, + } + m.pushNotificationServer = pushnotificationserver.New(config, pushNotificationServerPersistence, m.processor) + } + + return m.pushNotificationServer.Start() +} + +// StopPushNotificationServer stops the push notification server if running +func (m *Messenger) StopPushNotificationsServer() error { + m.pushNotificationServer = nil + return nil +} + func generateAliasAndIdenticon(pk string) (string, string, error) { identicon, err := identicon.GenerateBase64(pk) if err != nil { diff --git a/protocol/messenger_config.go b/protocol/messenger_config.go new file mode 100644 index 000000000..f404a11ae --- /dev/null +++ b/protocol/messenger_config.go @@ -0,0 +1,124 @@ +package protocol + +import ( + "database/sql" + + "go.uber.org/zap" + + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/encryption" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/pushnotificationclient" + "github.com/status-im/status-go/protocol/pushnotificationserver" + "github.com/status-im/status-go/protocol/transport" +) + +type config struct { + // This needs to be exposed until we move here mailserver logic + // as otherwise the client is not notified of a new filter and + // won't be pulling messages from mailservers until it reloads the chats/filters + onNegotiatedFilters func([]*transport.Filter) + // DEPRECATED: no need to expose it + onSendContactCodeHandler func(*encryption.ProtocolMessageSpec) + + // systemMessagesTranslations holds translations for system-messages + systemMessagesTranslations map[protobuf.MembershipUpdateEvent_EventType]string + // Config for the envelopes monitor + envelopesMonitorConfig *transport.EnvelopesMonitorConfig + + messagesPersistenceEnabled bool + featureFlags common.FeatureFlags + + // A path to a database or a database instance is required. + // The database instance has a higher priority. + dbConfig dbConfig + db *sql.DB + + verifyTransactionClient EthClient + + pushNotificationServerConfig *pushnotificationserver.Config + pushNotificationClientConfig *pushnotificationclient.Config + + logger *zap.Logger +} + +type Option func(*config) error + +// WithSystemMessagesTranslations is required for Group Chats which are currently disabled. +// nolint: unused +func WithSystemMessagesTranslations(t map[protobuf.MembershipUpdateEvent_EventType]string) Option { + return func(c *config) error { + c.systemMessagesTranslations = t + return nil + } +} + +func WithOnNegotiatedFilters(h func([]*transport.Filter)) Option { + return func(c *config) error { + c.onNegotiatedFilters = h + return nil + } +} + +func WithCustomLogger(logger *zap.Logger) Option { + return func(c *config) error { + c.logger = logger + return nil + } +} + +func WithMessagesPersistenceEnabled() Option { + return func(c *config) error { + c.messagesPersistenceEnabled = true + return nil + } +} + +func WithDatabaseConfig(dbPath, dbKey string) Option { + return func(c *config) error { + c.dbConfig = dbConfig{dbPath: dbPath, dbKey: dbKey} + return nil + } +} + +func WithVerifyTransactionClient(client EthClient) Option { + return func(c *config) error { + c.verifyTransactionClient = client + return nil + } +} + +func WithDatabase(db *sql.DB) Option { + return func(c *config) error { + c.db = db + return nil + } +} + +func WithPushNotificationServerConfig(pushNotificationServerConfig *pushnotificationserver.Config) Option { + return func(c *config) error { + c.pushNotificationServerConfig = pushNotificationServerConfig + return nil + } +} + +func WithPushNotificationClientConfig(pushNotificationClientConfig *pushnotificationclient.Config) Option { + return func(c *config) error { + c.pushNotificationClientConfig = pushNotificationClientConfig + return nil + } +} + +func WithDatasync() func(c *config) error { + return func(c *config) error { + c.featureFlags.Datasync = true + return nil + } +} + +func WithEnvelopesMonitorConfig(emc *transport.EnvelopesMonitorConfig) Option { + return func(c *config) error { + c.envelopesMonitorConfig = emc + return nil + } +} diff --git a/protocol/messenger_contact_update_test.go b/protocol/messenger_contact_update_test.go index 0b291e6e1..25a6e9de7 100644 --- a/protocol/messenger_contact_update_test.go +++ b/protocol/messenger_contact_update_test.go @@ -15,7 +15,7 @@ import ( "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/protocol/tt" - "github.com/status-im/status-go/whisper/v6" + "github.com/status-im/status-go/waku" ) func TestMessengerContactUpdateSuite(t *testing.T) { @@ -27,8 +27,8 @@ type MessengerContactUpdateSuite struct { m *Messenger // main instance of Messenger privateKey *ecdsa.PrivateKey // private key for the main instance of Messenger // If one wants to send messages between different instances of Messenger, - // a single Whisper service should be shared. - shh types.Whisper + // a single waku service should be shared. + shh types.Waku tmpFiles []*os.File // files to clean up logger *zap.Logger } @@ -36,17 +36,17 @@ type MessengerContactUpdateSuite struct { func (s *MessengerContactUpdateSuite) SetupTest() { s.logger = tt.MustCreateTestLogger() - config := whisper.DefaultConfig - config.MinimumAcceptedPOW = 0 - shh := whisper.New(&config) - s.shh = gethbridge.NewGethWhisperWrapper(shh) + config := waku.DefaultConfig + config.MinimumAcceptedPoW = 0 + shh := waku.New(&config, s.logger) + s.shh = gethbridge.NewGethWakuWrapper(shh) s.Require().NoError(shh.Start(nil)) s.m = s.newMessenger(s.shh) s.privateKey = s.m.identity } -func (s *MessengerContactUpdateSuite) newMessengerWithKey(shh types.Whisper, privateKey *ecdsa.PrivateKey) *Messenger { +func (s *MessengerContactUpdateSuite) newMessengerWithKey(shh types.Waku, privateKey *ecdsa.PrivateKey) *Messenger { tmpFile, err := ioutil.TempFile("", "") s.Require().NoError(err) @@ -72,7 +72,7 @@ func (s *MessengerContactUpdateSuite) newMessengerWithKey(shh types.Whisper, pri return m } -func (s *MessengerContactUpdateSuite) newMessenger(shh types.Whisper) *Messenger { +func (s *MessengerContactUpdateSuite) newMessenger(shh types.Waku) *Messenger { privateKey, err := crypto.GenerateKey() s.Require().NoError(err) diff --git a/protocol/messenger_installations_test.go b/protocol/messenger_installations_test.go index 514bafa37..8c572e058 100644 --- a/protocol/messenger_installations_test.go +++ b/protocol/messenger_installations_test.go @@ -17,9 +17,11 @@ import ( "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/protocol/encryption/multidevice" "github.com/status-im/status-go/protocol/tt" - "github.com/status-im/status-go/whisper/v6" + "github.com/status-im/status-go/waku" ) +const statusChatID = "status" + func TestMessengerInstallationSuite(t *testing.T) { suite.Run(t, new(MessengerInstallationSuite)) } @@ -30,8 +32,8 @@ type MessengerInstallationSuite struct { privateKey *ecdsa.PrivateKey // private key for the main instance of Messenger // If one wants to send messages between different instances of Messenger, - // a single Whisper service should be shared. - shh types.Whisper + // a single Waku service should be shared. + shh types.Waku tmpFiles []*os.File // files to clean up logger *zap.Logger @@ -40,17 +42,17 @@ type MessengerInstallationSuite struct { func (s *MessengerInstallationSuite) SetupTest() { s.logger = tt.MustCreateTestLogger() - config := whisper.DefaultConfig - config.MinimumAcceptedPOW = 0 - shh := whisper.New(&config) - s.shh = gethbridge.NewGethWhisperWrapper(shh) + config := waku.DefaultConfig + config.MinimumAcceptedPoW = 0 + shh := waku.New(&config, s.logger) + s.shh = gethbridge.NewGethWakuWrapper(shh) s.Require().NoError(shh.Start(nil)) s.m = s.newMessenger(s.shh) s.privateKey = s.m.identity } -func (s *MessengerInstallationSuite) newMessengerWithKey(shh types.Whisper, privateKey *ecdsa.PrivateKey) *Messenger { +func (s *MessengerInstallationSuite) newMessengerWithKey(shh types.Waku, privateKey *ecdsa.PrivateKey) *Messenger { tmpFile, err := ioutil.TempFile("", "") s.Require().NoError(err) @@ -77,7 +79,7 @@ func (s *MessengerInstallationSuite) newMessengerWithKey(shh types.Whisper, priv return m } -func (s *MessengerInstallationSuite) newMessenger(shh types.Whisper) *Messenger { +func (s *MessengerInstallationSuite) newMessenger(shh types.Waku) *Messenger { privateKey, err := crypto.GenerateKey() s.Require().NoError(err) @@ -136,7 +138,7 @@ func (s *MessengerInstallationSuite) TestReceiveInstallation() { s.Require().Equal(contact.ID, actualContact.ID) s.Require().True(actualContact.IsAdded()) - chat := CreatePublicChat("status", s.m.transport) + chat := CreatePublicChat(statusChatID, s.m.transport) err = s.m.SaveChat(&chat) s.Require().NoError(err) @@ -149,7 +151,7 @@ func (s *MessengerInstallationSuite) TestReceiveInstallation() { s.Require().NoError(err) actualChat := response.Chats[0] - s.Require().Equal("status", actualChat.ID) + s.Require().Equal(statusChatID, actualChat.ID) s.Require().True(actualChat.Active) } @@ -166,7 +168,7 @@ func (s *MessengerInstallationSuite) TestSyncInstallation() { s.Require().NoError(err) // add chat - chat := CreatePublicChat("status", s.m.transport) + chat := CreatePublicChat(statusChatID, s.m.transport) err = s.m.SaveChat(&chat) s.Require().NoError(err) @@ -230,7 +232,7 @@ func (s *MessengerInstallationSuite) TestSyncInstallation() { var statusChat *Chat for _, c := range allChats { - if c.ID == "status" { + if c.ID == statusChatID { statusChat = c } } diff --git a/protocol/messenger_mute_test.go b/protocol/messenger_mute_test.go new file mode 100644 index 000000000..8fd525771 --- /dev/null +++ b/protocol/messenger_mute_test.go @@ -0,0 +1,110 @@ +package protocol + +import ( + "crypto/ecdsa" + "io/ioutil" + "os" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" + "go.uber.org/zap" + + gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/tt" + "github.com/status-im/status-go/waku" +) + +func TestMessengerMuteSuite(t *testing.T) { + suite.Run(t, new(MessengerMuteSuite)) +} + +type MessengerMuteSuite struct { + suite.Suite + m *Messenger // main instance of Messenger + privateKey *ecdsa.PrivateKey // private key for the main instance of Messenger + + // If one wants to send messages between different instances of Messenger, + // a single Waku service should be shared. + shh types.Waku + + tmpFiles []*os.File // files to clean up + logger *zap.Logger +} + +func (s *MessengerMuteSuite) SetupTest() { + s.logger = tt.MustCreateTestLogger() + + config := waku.DefaultConfig + config.MinimumAcceptedPoW = 0 + shh := waku.New(&config, s.logger) + s.shh = gethbridge.NewGethWakuWrapper(shh) + s.Require().NoError(shh.Start(nil)) + + s.m = s.newMessenger(s.shh) + s.privateKey = s.m.identity +} + +func (s *MessengerMuteSuite) newMessengerWithKey(shh types.Waku, privateKey *ecdsa.PrivateKey) *Messenger { + tmpFile, err := ioutil.TempFile("", "") + s.Require().NoError(err) + + options := []Option{ + WithCustomLogger(s.logger), + WithMessagesPersistenceEnabled(), + WithDatabaseConfig(tmpFile.Name(), "some-key"), + WithDatasync(), + } + installationID := uuid.New().String() + m, err := NewMessenger( + privateKey, + &testNode{shh: shh}, + installationID, + options..., + ) + s.Require().NoError(err) + + err = m.Init() + s.Require().NoError(err) + + s.tmpFiles = append(s.tmpFiles, tmpFile) + + return m +} + +func (s *MessengerMuteSuite) newMessenger(shh types.Waku) *Messenger { + privateKey, err := crypto.GenerateKey() + s.Require().NoError(err) + + return s.newMessengerWithKey(s.shh, privateKey) +} + +func (s *MessengerMuteSuite) TestSetMute() { + key, err := crypto.GenerateKey() + s.Require().NoError(err) + + theirMessenger := s.newMessengerWithKey(s.shh, key) + + chatID := "status" + + chat := CreatePublicChat(chatID, s.m.transport) + + err = s.m.SaveChat(&chat) + s.Require().NoError(err) + + err = s.m.Join(chat) + s.Require().NoError(err) + + err = theirMessenger.SaveChat(&chat) + s.Require().NoError(err) + + s.Require().NoError(s.m.MuteChat(chatID)) + + s.Require().Len(s.m.Chats(), 1) + s.Require().True(s.m.Chats()[0].Muted) + + s.Require().NoError(s.m.UnmuteChat(chatID)) + s.Require().False(s.m.Chats()[0].Muted) +} diff --git a/protocol/messenger_test.go b/protocol/messenger_test.go index 441bbecd8..3eb834e52 100644 --- a/protocol/messenger_test.go +++ b/protocol/messenger_test.go @@ -27,7 +27,7 @@ import ( "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/tt" v1protocol "github.com/status-im/status-go/protocol/v1" - "github.com/status-im/status-go/whisper/v6" + "github.com/status-im/status-go/waku" ) const ( @@ -62,13 +62,13 @@ type MessengerSuite struct { privateKey *ecdsa.PrivateKey // private key for the main instance of Messenger // If one wants to send messages between different instances of Messenger, // a single Whisper service should be shared. - shh types.Whisper + shh types.Waku tmpFiles []*os.File // files to clean up logger *zap.Logger } type testNode struct { - shh types.Whisper + shh types.Waku } func (n *testNode) NewENSVerifier(_ *zap.Logger) enstypes.ENSVerifier { @@ -84,27 +84,27 @@ func (n *testNode) RemovePeer(_ string) error { } func (n *testNode) GetWaku(_ interface{}) (types.Waku, error) { - panic("not implemented") + return n.shh, nil } func (n *testNode) GetWhisper(_ interface{}) (types.Whisper, error) { - return n.shh, nil + return nil, nil } func (s *MessengerSuite) SetupTest() { s.logger = tt.MustCreateTestLogger() - config := whisper.DefaultConfig - config.MinimumAcceptedPOW = 0 - shh := whisper.New(&config) - s.shh = gethbridge.NewGethWhisperWrapper(shh) + config := waku.DefaultConfig + config.MinimumAcceptedPoW = 0 + shh := waku.New(&config, s.logger) + s.shh = gethbridge.NewGethWakuWrapper(shh) s.Require().NoError(shh.Start(nil)) s.m = s.newMessenger(s.shh) s.privateKey = s.m.identity } -func (s *MessengerSuite) newMessengerWithKey(shh types.Whisper, privateKey *ecdsa.PrivateKey) *Messenger { +func (s *MessengerSuite) newMessengerWithKey(shh types.Waku, privateKey *ecdsa.PrivateKey) *Messenger { tmpFile, err := ioutil.TempFile("", "") s.Require().NoError(err) @@ -132,7 +132,7 @@ func (s *MessengerSuite) newMessengerWithKey(shh types.Whisper, privateKey *ecds return m } -func (s *MessengerSuite) newMessenger(shh types.Whisper) *Messenger { +func (s *MessengerSuite) newMessenger(shh types.Waku) *Messenger { privateKey, err := crypto.GenerateKey() s.Require().NoError(err) return s.newMessengerWithKey(shh, privateKey) @@ -2056,7 +2056,7 @@ type MockEthClient struct { } type mockSendMessagesRequest struct { - types.Whisper + types.Waku req types.MessagesRequest } @@ -2101,12 +2101,13 @@ func (s *MessengerSuite) TestMessageJSON() { func (s *MessengerSuite) TestRequestHistoricMessagesRequest() { shh := &mockSendMessagesRequest{ - Whisper: s.shh, + Waku: s.shh, } m := s.newMessenger(shh) ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) defer cancel() - cursor, err := m.RequestHistoricMessages(ctx, nil, 10, 20, []byte{0x01}) + m.mailserver = []byte("mailserver-id") + cursor, err := m.RequestHistoricMessages(ctx, 10, 20, []byte{0x01}) s.EqualError(err, ctx.Err().Error()) s.Empty(cursor) // verify request is correct diff --git a/protocol/migrations/migrations.go b/protocol/migrations/migrations.go index e9c4e7aa5..0831a664d 100644 --- a/protocol/migrations/migrations.go +++ b/protocol/migrations/migrations.go @@ -12,6 +12,8 @@ // 1589365189_add_pow_target.up.sql (66B) // 1591277220_add_index_messages.down.sql (237B) // 1591277220_add_index_messages.up.sql (240B) +// 1593087212_add_mute_chat_and_raw_message_fields.down.sql (0) +// 1593087212_add_mute_chat_and_raw_message_fields.up.sql (215B) // doc.go (850B) package migrations @@ -136,7 +138,7 @@ func _000002_add_last_ens_clock_valueDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000002_add_last_ens_clock_value.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1584434371, 0)} + info := bindataFileInfo{name: "000002_add_last_ens_clock_value.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}} return a, nil } @@ -156,7 +158,7 @@ func _000002_add_last_ens_clock_valueUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "000002_add_last_ens_clock_value.up.sql", size: 77, mode: os.FileMode(0644), modTime: time.Unix(1584434371, 0)} + info := bindataFileInfo{name: "000002_add_last_ens_clock_value.up.sql", size: 77, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x4d, 0x3, 0x8f, 0xd5, 0x85, 0x83, 0x47, 0xbe, 0xf9, 0x82, 0x7e, 0x81, 0xa4, 0xbd, 0xaa, 0xd5, 0x98, 0x18, 0x5, 0x2d, 0x82, 0x42, 0x3b, 0x3, 0x50, 0xc3, 0x1e, 0x84, 0x35, 0xf, 0xb6, 0x2b}} return a, nil } @@ -176,7 +178,7 @@ func _1586358095_add_replaceDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1586358095_add_replace.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1589265610, 0)} + info := bindataFileInfo{name: "1586358095_add_replace.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}} return a, nil } @@ -196,7 +198,7 @@ func _1586358095_add_replaceUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1586358095_add_replace.up.sql", size: 224, mode: os.FileMode(0644), modTime: time.Unix(1589265610, 0)} + info := bindataFileInfo{name: "1586358095_add_replace.up.sql", size: 224, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xd2, 0xb3, 0xa9, 0xc7, 0x7f, 0x9d, 0x8f, 0x43, 0x8c, 0x9e, 0x58, 0x8d, 0x44, 0xbc, 0xfa, 0x6b, 0x5f, 0x3f, 0x5a, 0xbe, 0xe8, 0xb1, 0x16, 0xf, 0x91, 0x2a, 0xa0, 0x71, 0xbb, 0x8d, 0x6b, 0xcb}} return a, nil } @@ -216,7 +218,7 @@ func _1588665364_add_image_dataDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1588665364_add_image_data.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1591361643, 0)} + info := bindataFileInfo{name: "1588665364_add_image_data.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}} return a, nil } @@ -236,7 +238,7 @@ func _1588665364_add_image_dataUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1588665364_add_image_data.up.sql", size: 186, mode: os.FileMode(0644), modTime: time.Unix(1591361643, 0)} + info := bindataFileInfo{name: "1588665364_add_image_data.up.sql", size: 186, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xd6, 0xc6, 0x35, 0xb4, 0x4c, 0x39, 0x96, 0x29, 0x30, 0xda, 0xf4, 0x8f, 0xcb, 0xf1, 0x9f, 0x84, 0xdc, 0x88, 0xd4, 0xd5, 0xbc, 0xb6, 0x5b, 0x46, 0x78, 0x67, 0x76, 0x1a, 0x5, 0x36, 0xdc, 0xe5}} return a, nil } @@ -256,7 +258,7 @@ func _1589365189_add_pow_targetDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1589365189_add_pow_target.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1591361643, 0)} + info := bindataFileInfo{name: "1589365189_add_pow_target.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}} return a, nil } @@ -276,7 +278,7 @@ func _1589365189_add_pow_targetUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1589365189_add_pow_target.up.sql", size: 66, mode: os.FileMode(0644), modTime: time.Unix(1591361643, 0)} + info := bindataFileInfo{name: "1589365189_add_pow_target.up.sql", size: 66, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x4e, 0x3a, 0xe2, 0x2e, 0x7d, 0xaf, 0xbb, 0xcc, 0x21, 0xa1, 0x7a, 0x41, 0x9a, 0xd0, 0xbb, 0xa9, 0xc8, 0x35, 0xf9, 0x32, 0x34, 0x46, 0x44, 0x9a, 0x86, 0x40, 0x7c, 0xb9, 0x23, 0xc7, 0x3, 0x3f}} return a, nil } @@ -296,7 +298,7 @@ func _1591277220_add_index_messagesDownSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1591277220_add_index_messages.down.sql", size: 237, mode: os.FileMode(0644), modTime: time.Unix(1591361643, 0)} + info := bindataFileInfo{name: "1591277220_add_index_messages.down.sql", size: 237, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x79, 0xe5, 0x42, 0x56, 0x64, 0x1d, 0xb7, 0x8a, 0x1b, 0x0, 0x99, 0xf0, 0x18, 0x8c, 0x69, 0xe3, 0x14, 0x3a, 0x7f, 0x78, 0xfe, 0xe3, 0x2e, 0xcb, 0x6e, 0x5c, 0x8c, 0x1f, 0x7b, 0xfc, 0x21, 0xc7}} return a, nil } @@ -316,11 +318,51 @@ func _1591277220_add_index_messagesUpSql() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "1591277220_add_index_messages.up.sql", size: 240, mode: os.FileMode(0644), modTime: time.Unix(1591361844, 0)} + info := bindataFileInfo{name: "1591277220_add_index_messages.up.sql", size: 240, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x9c, 0xfe, 0xbe, 0xd5, 0xb8, 0x8f, 0xdd, 0xef, 0xbb, 0xa8, 0xad, 0x7f, 0xed, 0x5b, 0x5b, 0x2f, 0xe6, 0x82, 0x27, 0x78, 0x1f, 0xb9, 0x57, 0xdc, 0x8, 0xc2, 0xb2, 0xa9, 0x9a, 0x4, 0xe1, 0x7a}} return a, nil } +var __1593087212_add_mute_chat_and_raw_message_fieldsDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00") + +func _1593087212_add_mute_chat_and_raw_message_fieldsDownSqlBytes() ([]byte, error) { + return bindataRead( + __1593087212_add_mute_chat_and_raw_message_fieldsDownSql, + "1593087212_add_mute_chat_and_raw_message_fields.down.sql", + ) +} + +func _1593087212_add_mute_chat_and_raw_message_fieldsDownSql() (*asset, error) { + bytes, err := _1593087212_add_mute_chat_and_raw_message_fieldsDownSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "1593087212_add_mute_chat_and_raw_message_fields.down.sql", size: 0, mode: os.FileMode(0644), modTime: time.Unix(1595832279, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}} + return a, nil +} + +var __1593087212_add_mute_chat_and_raw_message_fieldsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xa4\xcc\x31\x0e\xc2\x30\x0c\x00\xc0\x9d\x57\xf8\x1f\x4c\x2e\x71\x27\xd3\x48\x90\xce\x51\x94\x1a\x1a\xa1\xa6\x51\xed\x0a\xf1\x7b\x56\x58\x58\x78\xc0\x1d\x72\xa0\x0b\x04\xec\x98\x20\xcf\xc9\x14\xd0\x39\x38\x79\x1e\xcf\x03\x2c\xbb\xc9\x04\x9d\xf7\x4c\x38\x80\xa3\x1e\x47\x0e\xd0\x23\x5f\xe9\x78\xf8\x94\x5b\x7a\xc6\x45\x54\xd3\x5d\xbe\x02\x7d\x94\x16\xa5\xe6\xed\xd5\xac\xac\xf5\xaf\x4a\xea\x14\xdb\xae\x73\xac\xab\x95\x5b\xc9\xe9\xd7\xf8\x0e\x00\x00\xff\xff\xd9\x47\x38\x58\xd7\x00\x00\x00") + +func _1593087212_add_mute_chat_and_raw_message_fieldsUpSqlBytes() ([]byte, error) { + return bindataRead( + __1593087212_add_mute_chat_and_raw_message_fieldsUpSql, + "1593087212_add_mute_chat_and_raw_message_fields.up.sql", + ) +} + +func _1593087212_add_mute_chat_and_raw_message_fieldsUpSql() (*asset, error) { + bytes, err := _1593087212_add_mute_chat_and_raw_message_fieldsUpSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "1593087212_add_mute_chat_and_raw_message_fields.up.sql", size: 215, mode: os.FileMode(0644), modTime: time.Unix(1595832279, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x73, 0x99, 0x61, 0xd1, 0xaa, 0xb4, 0xbf, 0xaf, 0xd7, 0x20, 0x17, 0x40, 0xf9, 0x2, 0xfb, 0xcc, 0x40, 0x2a, 0xd, 0x86, 0x36, 0x30, 0x88, 0x89, 0x25, 0x80, 0x42, 0xb0, 0x5b, 0xe9, 0x73, 0x78}} + return a, nil +} + var _docGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x52\x3f\x8f\xdb\x3e\x0c\xdd\xf3\x29\x1e\x6e\xb9\xe5\x22\x07\xf8\xfd\xa6\xdb\x3a\x74\xe8\xd2\x2e\xd9\x0b\x46\xa6\x6d\x22\x32\xe5\x8a\xf4\x39\xf9\xf6\x85\x74\x17\x9c\x51\x14\xe8\x4a\x89\x8f\xef\x5f\xd7\xe1\x3c\x89\x61\x90\xc4\x10\x83\x72\x64\x33\x2a\x77\x5c\x38\xd2\x6a\x8c\xa7\x51\x7c\x5a\x2f\x21\xe6\xb9\x33\x27\x5f\xed\x28\x73\x37\xcb\x58\xc8\xb9\x7b\xfb\xff\xe9\xd0\x75\x88\xa4\xcf\x8e\x89\xb4\x4f\xdc\xb0\x0c\xe6\x54\x5c\x74\xc4\x26\x3e\x81\xb0\x14\x1e\xe4\x16\xf0\xc5\x91\x98\xcc\xe1\x13\xf9\xb3\xc1\x27\x46\x24\xe3\x0a\x33\xe4\x82\x31\x1f\x2f\xa2\x3d\x39\x85\x3a\xfa\x36\xec\x26\x95\x61\xa4\x94\xb8\xc7\x50\xf2\xdc\x76\x8d\x66\x46\x2f\x85\xa3\xe7\x72\x7f\x01\x99\xb1\x43\x69\x66\xab\xfb\x13\xbd\x31\x34\x7f\x9c\x07\x69\xff\x6f\x45\xd8\x72\xb9\x1a\xc8\xc0\xb7\x85\xa3\x73\x1f\x0e\x15\xeb\xfb\x8f\xf3\xd7\x57\x9c\x27\xae\xf0\x55\x5a\x1e\x1a\x85\x66\x9e\x32\xf7\x06\xcf\x18\x72\x4a\x79\x6b\x0f\xab\xca\x0d\x2e\x33\x9b\xd3\xbc\x20\x66\x7d\x63\x75\xc9\x5a\xd1\x56\x4d\x72\xe5\xf6\xcf\xb7\x0c\x51\x71\xa1\xf4\xee\x5e\x93\x7e\x7e\x37\xe8\x11\x44\x5c\x4b\x61\xf5\x74\x6f\x2b\xac\xb1\xdc\x97\x8a\x85\x77\xe6\x92\xd5\x9a\xbc\xa5\x64\xcf\x31\xa7\xdd\xbc\xa2\xd9\x44\x85\x3f\x1d\x73\xba\x24\x7e\xc1\x36\x49\x9c\x30\x33\xa9\xb5\x40\xda\x87\x44\xce\xe6\x9f\xfb\x10\x85\x73\x99\xad\x0a\xae\xfc\xaa\xbb\x15\xb3\x16\xe7\x91\xc3\x8e\x50\x33\x7f\xa1\xf8\x51\x85\xc7\x95\xd5\xd8\x40\x7f\x98\xf2\x08\x79\x63\x50\xdf\xe3\x74\x3a\x9d\xfe\xfb\x19\x42\x68\x5d\xe0\x1b\xcd\x4b\xa5\xe9\xb5\xa3\x9b\xa4\x84\x0b\x43\x46\xcd\x85\xfb\xca\x8a\x6f\x62\xad\x64\x31\x09\xab\xd7\xcc\x2a\x5e\x4e\x3d\x97\xaa\x47\xf7\x7a\xfe\x66\x59\x38\x1c\x16\x8a\x57\x1a\x19\xf6\x2b\x89\x73\x0d\x7a\xcc\xaf\x23\x2b\xd7\x3a\xec\xcb\x77\x5c\xae\xe3\xde\xec\x63\x46\x08\xdd\xe7\x20\x8c\x19\xe1\xf0\x3b\x00\x00\xff\xff\x12\xcd\x7f\xc4\x52\x03\x00\x00") func docGoBytes() ([]byte, error) { @@ -336,7 +378,7 @@ func docGo() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "doc.go", size: 850, mode: os.FileMode(0644), modTime: time.Unix(1589265610, 0)} + info := bindataFileInfo{name: "doc.go", size: 850, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xa0, 0xcc, 0x41, 0xe1, 0x61, 0x12, 0x97, 0xe, 0x36, 0x8c, 0xa7, 0x9e, 0xe0, 0x6e, 0x59, 0x9e, 0xee, 0xd5, 0x4a, 0xcf, 0x1e, 0x60, 0xd6, 0xc3, 0x3a, 0xc9, 0x6c, 0xf2, 0x86, 0x5a, 0xb4, 0x1e}} return a, nil } @@ -456,6 +498,10 @@ var _bindata = map[string]func() (*asset, error){ "1591277220_add_index_messages.up.sql": _1591277220_add_index_messagesUpSql, + "1593087212_add_mute_chat_and_raw_message_fields.down.sql": _1593087212_add_mute_chat_and_raw_message_fieldsDownSql, + + "1593087212_add_mute_chat_and_raw_message_fields.up.sql": _1593087212_add_mute_chat_and_raw_message_fieldsUpSql, + "doc.go": docGo, } @@ -500,19 +546,21 @@ type bintree struct { } var _bintree = &bintree{nil, map[string]*bintree{ - "000001_init.down.db.sql": &bintree{_000001_initDownDbSql, map[string]*bintree{}}, - "000001_init.up.db.sql": &bintree{_000001_initUpDbSql, map[string]*bintree{}}, - "000002_add_last_ens_clock_value.down.sql": &bintree{_000002_add_last_ens_clock_valueDownSql, map[string]*bintree{}}, - "000002_add_last_ens_clock_value.up.sql": &bintree{_000002_add_last_ens_clock_valueUpSql, map[string]*bintree{}}, - "1586358095_add_replace.down.sql": &bintree{_1586358095_add_replaceDownSql, map[string]*bintree{}}, - "1586358095_add_replace.up.sql": &bintree{_1586358095_add_replaceUpSql, map[string]*bintree{}}, - "1588665364_add_image_data.down.sql": &bintree{_1588665364_add_image_dataDownSql, map[string]*bintree{}}, - "1588665364_add_image_data.up.sql": &bintree{_1588665364_add_image_dataUpSql, map[string]*bintree{}}, - "1589365189_add_pow_target.down.sql": &bintree{_1589365189_add_pow_targetDownSql, map[string]*bintree{}}, - "1589365189_add_pow_target.up.sql": &bintree{_1589365189_add_pow_targetUpSql, map[string]*bintree{}}, - "1591277220_add_index_messages.down.sql": &bintree{_1591277220_add_index_messagesDownSql, map[string]*bintree{}}, - "1591277220_add_index_messages.up.sql": &bintree{_1591277220_add_index_messagesUpSql, map[string]*bintree{}}, - "doc.go": &bintree{docGo, map[string]*bintree{}}, + "000001_init.down.db.sql": &bintree{_000001_initDownDbSql, map[string]*bintree{}}, + "000001_init.up.db.sql": &bintree{_000001_initUpDbSql, map[string]*bintree{}}, + "000002_add_last_ens_clock_value.down.sql": &bintree{_000002_add_last_ens_clock_valueDownSql, map[string]*bintree{}}, + "000002_add_last_ens_clock_value.up.sql": &bintree{_000002_add_last_ens_clock_valueUpSql, map[string]*bintree{}}, + "1586358095_add_replace.down.sql": &bintree{_1586358095_add_replaceDownSql, map[string]*bintree{}}, + "1586358095_add_replace.up.sql": &bintree{_1586358095_add_replaceUpSql, map[string]*bintree{}}, + "1588665364_add_image_data.down.sql": &bintree{_1588665364_add_image_dataDownSql, map[string]*bintree{}}, + "1588665364_add_image_data.up.sql": &bintree{_1588665364_add_image_dataUpSql, map[string]*bintree{}}, + "1589365189_add_pow_target.down.sql": &bintree{_1589365189_add_pow_targetDownSql, map[string]*bintree{}}, + "1589365189_add_pow_target.up.sql": &bintree{_1589365189_add_pow_targetUpSql, map[string]*bintree{}}, + "1591277220_add_index_messages.down.sql": &bintree{_1591277220_add_index_messagesDownSql, map[string]*bintree{}}, + "1591277220_add_index_messages.up.sql": &bintree{_1591277220_add_index_messagesUpSql, map[string]*bintree{}}, + "1593087212_add_mute_chat_and_raw_message_fields.down.sql": &bintree{_1593087212_add_mute_chat_and_raw_message_fieldsDownSql, map[string]*bintree{}}, + "1593087212_add_mute_chat_and_raw_message_fields.up.sql": &bintree{_1593087212_add_mute_chat_and_raw_message_fieldsUpSql, map[string]*bintree{}}, + "doc.go": &bintree{docGo, map[string]*bintree{}}, }} // RestoreAsset restores an asset under the given directory. diff --git a/protocol/migrations/sqlite/1593087212_add_mute_chat_and_raw_message_fields.down.sql b/protocol/migrations/sqlite/1593087212_add_mute_chat_and_raw_message_fields.down.sql new file mode 100644 index 000000000..e69de29bb diff --git a/protocol/migrations/sqlite/1593087212_add_mute_chat_and_raw_message_fields.up.sql b/protocol/migrations/sqlite/1593087212_add_mute_chat_and_raw_message_fields.up.sql new file mode 100644 index 000000000..20fc7405d --- /dev/null +++ b/protocol/migrations/sqlite/1593087212_add_mute_chat_and_raw_message_fields.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE chats ADD COLUMN muted BOOLEAN DEFAULT FALSE; +ALTER TABLE raw_messages ADD COLUMN skip_encryption BOOLEAN DEFAULT FALSE; +ALTER TABLE raw_messages ADD COLUMN send_push_notification BOOLEAN DEFAULT FALSE; diff --git a/protocol/persistence.go b/protocol/persistence.go index 9ef0720ba..7ed18fd96 100644 --- a/protocol/persistence.go +++ b/protocol/persistence.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/protocol/common" ) var ( @@ -103,8 +104,8 @@ func (db sqlitePersistence) saveChat(tx *sql.Tx, chat Chat) error { } // Insert record - stmt, err := tx.Prepare(`INSERT INTO chats(id, name, color, active, type, timestamp, deleted_at_clock_value, unviewed_message_count, last_clock_value, last_message, members, membership_updates) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) + stmt, err := tx.Prepare(`INSERT INTO chats(id, name, color, active, type, timestamp, deleted_at_clock_value, unviewed_message_count, last_clock_value, last_message, members, membership_updates, muted) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?)`) if err != nil { return err } @@ -123,6 +124,7 @@ func (db sqlitePersistence) saveChat(tx *sql.Tx, chat Chat) error { chat.LastMessage, encodedMembers.Bytes(), encodedMembershipUpdates.Bytes(), + chat.Muted, ) if err != nil { return err @@ -136,6 +138,16 @@ func (db sqlitePersistence) DeleteChat(chatID string) error { return err } +func (db sqlitePersistence) MuteChat(chatID string) error { + _, err := db.db.Exec("UPDATE chats SET muted = 1 WHERE id = ?", chatID) + return err +} + +func (db sqlitePersistence) UnmuteChat(chatID string) error { + _, err := db.db.Exec("UPDATE chats SET muted = 0 WHERE id = ?", chatID) + return err +} + func (db sqlitePersistence) Chats() ([]*Chat, error) { return db.chats(nil) } @@ -170,6 +182,7 @@ func (db sqlitePersistence) chats(tx *sql.Tx) (chats []*Chat, err error) { chats.last_message, chats.members, chats.membership_updates, + chats.muted, contacts.identicon, contacts.alias FROM chats LEFT JOIN contacts ON chats.id = contacts.id @@ -201,6 +214,7 @@ func (db sqlitePersistence) chats(tx *sql.Tx) (chats []*Chat, err error) { &chat.LastMessage, &encodedMembers, &encodedMembershipUpdates, + &chat.Muted, &identicon, &alias, ) @@ -251,7 +265,8 @@ func (db sqlitePersistence) Chat(chatID string) (*Chat, error) { last_clock_value, last_message, members, - membership_updates + membership_updates, + muted FROM chats WHERE id = ? `, chatID).Scan(&chat.ID, @@ -266,6 +281,7 @@ func (db sqlitePersistence) Chat(chatID string) (*Chat, error) { &chat.LastMessage, &encodedMembers, &encodedMembershipUpdates, + &chat.Muted, ) switch err { case sql.ErrNoRows: @@ -362,7 +378,7 @@ func (db sqlitePersistence) Contacts() ([]*Contact, error) { return response, nil } -func (db sqlitePersistence) SaveRawMessage(message *RawMessage) error { +func (db sqlitePersistence) SaveRawMessage(message *common.RawMessage) error { var pubKeys [][]byte for _, pk := range message.Recipients { pubKeys = append(pubKeys, crypto.CompressPubkey(pk)) @@ -387,9 +403,11 @@ func (db sqlitePersistence) SaveRawMessage(message *RawMessage) error { message_type, resend_automatically, recipients, + skip_encryption, + send_push_notification, payload ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, message.ID, message.LocalChatID, message.LastSent, @@ -398,14 +416,16 @@ func (db sqlitePersistence) SaveRawMessage(message *RawMessage) error { message.MessageType, message.ResendAutomatically, encodedRecipients.Bytes(), + message.SkipEncryption, + message.SendPushNotification, message.Payload) return err } -func (db sqlitePersistence) RawMessageByID(id string) (*RawMessage, error) { +func (db sqlitePersistence) RawMessageByID(id string) (*common.RawMessage, error) { var rawPubKeys [][]byte var encodedRecipients []byte - message := &RawMessage{} + message := &common.RawMessage{} err := db.db.QueryRow(` SELECT @@ -417,6 +437,8 @@ func (db sqlitePersistence) RawMessageByID(id string) (*RawMessage, error) { message_type, resend_automatically, recipients, + skip_encryption, + send_push_notification, payload FROM raw_messages @@ -432,6 +454,8 @@ func (db sqlitePersistence) RawMessageByID(id string) (*RawMessage, error) { &message.MessageType, &message.ResendAutomatically, &encodedRecipients, + &message.SkipEncryption, + &message.SendPushNotification, &message.Payload, ) if err != nil { diff --git a/protocol/protobuf/application_metadata_message.pb.go b/protocol/protobuf/application_metadata_message.pb.go index b166e283f..37822b86a 100644 --- a/protocol/protobuf/application_metadata_message.pb.go +++ b/protocol/protobuf/application_metadata_message.pb.go @@ -38,6 +38,13 @@ const ( ApplicationMetadataMessage_SYNC_INSTALLATION_CONTACT ApplicationMetadataMessage_Type = 12 ApplicationMetadataMessage_SYNC_INSTALLATION_ACCOUNT ApplicationMetadataMessage_Type = 13 ApplicationMetadataMessage_SYNC_INSTALLATION_PUBLIC_CHAT ApplicationMetadataMessage_Type = 14 + ApplicationMetadataMessage_CONTACT_CODE_ADVERTISEMENT ApplicationMetadataMessage_Type = 15 + ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION ApplicationMetadataMessage_Type = 16 + ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION_RESPONSE ApplicationMetadataMessage_Type = 17 + ApplicationMetadataMessage_PUSH_NOTIFICATION_QUERY ApplicationMetadataMessage_Type = 18 + ApplicationMetadataMessage_PUSH_NOTIFICATION_QUERY_RESPONSE ApplicationMetadataMessage_Type = 19 + ApplicationMetadataMessage_PUSH_NOTIFICATION_REQUEST ApplicationMetadataMessage_Type = 20 + ApplicationMetadataMessage_PUSH_NOTIFICATION_RESPONSE ApplicationMetadataMessage_Type = 21 ) var ApplicationMetadataMessage_Type_name = map[int32]string{ @@ -56,6 +63,13 @@ var ApplicationMetadataMessage_Type_name = map[int32]string{ 12: "SYNC_INSTALLATION_CONTACT", 13: "SYNC_INSTALLATION_ACCOUNT", 14: "SYNC_INSTALLATION_PUBLIC_CHAT", + 15: "CONTACT_CODE_ADVERTISEMENT", + 16: "PUSH_NOTIFICATION_REGISTRATION", + 17: "PUSH_NOTIFICATION_REGISTRATION_RESPONSE", + 18: "PUSH_NOTIFICATION_QUERY", + 19: "PUSH_NOTIFICATION_QUERY_RESPONSE", + 20: "PUSH_NOTIFICATION_REQUEST", + 21: "PUSH_NOTIFICATION_RESPONSE", } var ApplicationMetadataMessage_Type_value = map[string]int32{ @@ -74,6 +88,13 @@ var ApplicationMetadataMessage_Type_value = map[string]int32{ "SYNC_INSTALLATION_CONTACT": 12, "SYNC_INSTALLATION_ACCOUNT": 13, "SYNC_INSTALLATION_PUBLIC_CHAT": 14, + "CONTACT_CODE_ADVERTISEMENT": 15, + "PUSH_NOTIFICATION_REGISTRATION": 16, + "PUSH_NOTIFICATION_REGISTRATION_RESPONSE": 17, + "PUSH_NOTIFICATION_QUERY": 18, + "PUSH_NOTIFICATION_QUERY_RESPONSE": 19, + "PUSH_NOTIFICATION_REQUEST": 20, + "PUSH_NOTIFICATION_RESPONSE": 21, } func (x ApplicationMetadataMessage_Type) String() string { @@ -150,29 +171,34 @@ func init() { func init() { proto.RegisterFile("application_metadata_message.proto", fileDescriptor_ad09a6406fcf24c7) } var fileDescriptor_ad09a6406fcf24c7 = []byte{ - // 377 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x91, 0xcf, 0x8e, 0xd3, 0x30, - 0x10, 0xc6, 0xc9, 0x36, 0x6c, 0x77, 0x67, 0x4b, 0x65, 0x06, 0x10, 0xe1, 0xcf, 0x6a, 0x97, 0x22, - 0xc1, 0x02, 0x52, 0x0e, 0x70, 0xe6, 0xe0, 0x75, 0x0c, 0x1b, 0x91, 0x38, 0xc1, 0x76, 0x84, 0x38, - 0x59, 0x2e, 0x0d, 0x55, 0xa5, 0xb6, 0x89, 0xda, 0xf4, 0xd0, 0x07, 0xe3, 0x29, 0x78, 0x29, 0x94, - 0xd0, 0xd2, 0x96, 0x82, 0x7a, 0xb2, 0xe6, 0xfb, 0x7e, 0x9f, 0x47, 0x33, 0x03, 0x3d, 0x5b, 0x96, - 0xe3, 0xd1, 0x37, 0x5b, 0x8d, 0x8a, 0xa9, 0x99, 0xe4, 0x95, 0x1d, 0xd8, 0xca, 0x9a, 0x49, 0x3e, - 0x9f, 0xdb, 0x61, 0xee, 0x97, 0xb3, 0xa2, 0x2a, 0xf0, 0xa4, 0x79, 0xfa, 0x8b, 0xef, 0xbd, 0x9f, - 0x2e, 0x3c, 0xa6, 0x9b, 0x40, 0xbc, 0xe2, 0xe3, 0xdf, 0x38, 0x3e, 0x85, 0xd3, 0xf9, 0x68, 0x38, - 0xb5, 0xd5, 0x62, 0x96, 0x7b, 0xce, 0xa5, 0x73, 0xd5, 0x91, 0x1b, 0x01, 0x3d, 0x68, 0x97, 0x76, - 0x39, 0x2e, 0xec, 0xc0, 0x3b, 0x6a, 0xbc, 0x75, 0x89, 0xef, 0xc1, 0xad, 0x96, 0x65, 0xee, 0xb5, - 0x2e, 0x9d, 0xab, 0xee, 0xdb, 0x57, 0xfe, 0xba, 0x9f, 0xff, 0xff, 0x5e, 0xbe, 0x5e, 0x96, 0xb9, - 0x6c, 0x62, 0xbd, 0x1f, 0x2d, 0x70, 0xeb, 0x12, 0xcf, 0xa0, 0x9d, 0x89, 0x4f, 0x22, 0xf9, 0x22, - 0xc8, 0x2d, 0x24, 0xd0, 0x61, 0x37, 0x54, 0x9b, 0x98, 0x2b, 0x45, 0x3f, 0x72, 0xe2, 0x20, 0x42, - 0x97, 0x25, 0x42, 0x53, 0xa6, 0x4d, 0x96, 0x06, 0x54, 0x73, 0x72, 0x84, 0xe7, 0xf0, 0x28, 0xe6, - 0xf1, 0x35, 0x97, 0xea, 0x26, 0x4c, 0x57, 0xf2, 0x9f, 0x48, 0x0b, 0x1f, 0xc0, 0xdd, 0x94, 0x86, - 0xd2, 0x84, 0x42, 0x69, 0x1a, 0x45, 0x54, 0x87, 0x89, 0x20, 0x6e, 0x2d, 0xab, 0xaf, 0x82, 0xed, - 0xca, 0xb7, 0xf1, 0x39, 0x5c, 0x48, 0xfe, 0x39, 0xe3, 0x4a, 0x1b, 0x1a, 0x04, 0x92, 0x2b, 0x65, - 0x3e, 0x24, 0xd2, 0x68, 0x49, 0x85, 0xa2, 0xac, 0x81, 0x8e, 0xf1, 0x35, 0xbc, 0xa0, 0x8c, 0xf1, - 0x54, 0x9b, 0x43, 0x6c, 0x1b, 0xdf, 0xc0, 0xcb, 0x80, 0xb3, 0x28, 0x14, 0xfc, 0x20, 0x7c, 0x82, - 0x0f, 0xe1, 0xde, 0x1a, 0xda, 0x36, 0x4e, 0xf1, 0x3e, 0x10, 0xc5, 0x45, 0xb0, 0xa3, 0x02, 0x5e, - 0xc0, 0x93, 0xbf, 0xff, 0xde, 0x06, 0xce, 0xea, 0xd5, 0xec, 0x0d, 0x69, 0x56, 0x0b, 0x24, 0x9d, - 0x7f, 0xdb, 0x94, 0xb1, 0x24, 0x13, 0x9a, 0xdc, 0xc1, 0x67, 0x70, 0xbe, 0x6f, 0xa7, 0xd9, 0x75, - 0x14, 0x32, 0x53, 0xdf, 0x85, 0x74, 0xfb, 0xc7, 0xcd, 0x9d, 0xdf, 0xfd, 0x0a, 0x00, 0x00, 0xff, - 0xff, 0xb7, 0x6c, 0xd6, 0xba, 0x84, 0x02, 0x00, 0x00, + // 463 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x92, 0x51, 0x4f, 0x13, 0x41, + 0x10, 0xc7, 0x2d, 0x94, 0x16, 0x86, 0x5a, 0x97, 0x01, 0x42, 0x05, 0x81, 0x5a, 0x8d, 0xa2, 0x26, + 0x7d, 0xd0, 0x67, 0x1f, 0x96, 0xbd, 0x81, 0x6e, 0xec, 0xed, 0x1d, 0xbb, 0x7b, 0x1a, 0x9e, 0x36, + 0x87, 0x9c, 0xa4, 0x09, 0xd0, 0x0b, 0x3d, 0x1e, 0xfa, 0x8d, 0xfc, 0x14, 0x7e, 0x36, 0x73, 0xd7, + 0xd6, 0x82, 0x2d, 0xf0, 0x74, 0xd9, 0xff, 0xff, 0x37, 0x33, 0x99, 0xff, 0x1c, 0xb4, 0xe2, 0x34, + 0xbd, 0xec, 0xfd, 0x8c, 0xb3, 0x5e, 0xff, 0xda, 0x5d, 0x25, 0x59, 0x7c, 0x1e, 0x67, 0xb1, 0xbb, + 0x4a, 0x06, 0x83, 0xf8, 0x22, 0x69, 0xa7, 0x37, 0xfd, 0xac, 0x8f, 0xcb, 0xc5, 0xe7, 0xec, 0xf6, + 0x57, 0xeb, 0x4f, 0x05, 0xb6, 0xf9, 0xb4, 0xc0, 0x1f, 0xf3, 0xfe, 0x08, 0xc7, 0x57, 0xb0, 0x32, + 0xe8, 0x5d, 0x5c, 0xc7, 0xd9, 0xed, 0x4d, 0xd2, 0x28, 0x35, 0x4b, 0x07, 0x35, 0x3d, 0x15, 0xb0, + 0x01, 0xd5, 0x34, 0x1e, 0x5e, 0xf6, 0xe3, 0xf3, 0xc6, 0x42, 0xe1, 0x4d, 0x9e, 0xf8, 0x15, 0xca, + 0xd9, 0x30, 0x4d, 0x1a, 0x8b, 0xcd, 0xd2, 0x41, 0xfd, 0xf3, 0x87, 0xf6, 0x64, 0x5e, 0xfb, 0xe1, + 0x59, 0x6d, 0x3b, 0x4c, 0x13, 0x5d, 0x94, 0xb5, 0x7e, 0x2f, 0x41, 0x39, 0x7f, 0xe2, 0x2a, 0x54, + 0x23, 0xf5, 0x4d, 0x05, 0x3f, 0x14, 0x7b, 0x86, 0x0c, 0x6a, 0xa2, 0xc3, 0xad, 0xf3, 0xc9, 0x18, + 0x7e, 0x4c, 0xac, 0x84, 0x08, 0x75, 0x11, 0x28, 0xcb, 0x85, 0x75, 0x51, 0xe8, 0x71, 0x4b, 0x6c, + 0x01, 0x77, 0xe1, 0xa5, 0x4f, 0xfe, 0x21, 0x69, 0xd3, 0x91, 0xe1, 0x58, 0xfe, 0x57, 0xb2, 0x88, + 0x9b, 0xb0, 0x16, 0x72, 0xa9, 0x9d, 0x54, 0xc6, 0xf2, 0x6e, 0x97, 0x5b, 0x19, 0x28, 0x56, 0xce, + 0x65, 0x73, 0xaa, 0xc4, 0x7d, 0x79, 0x09, 0xdf, 0xc0, 0xbe, 0xa6, 0x93, 0x88, 0x8c, 0x75, 0xdc, + 0xf3, 0x34, 0x19, 0xe3, 0x8e, 0x02, 0xed, 0xac, 0xe6, 0xca, 0x70, 0x51, 0x40, 0x15, 0xfc, 0x08, + 0xef, 0xb8, 0x10, 0x14, 0x5a, 0xf7, 0x14, 0x5b, 0xc5, 0x4f, 0xf0, 0xde, 0x23, 0xd1, 0x95, 0x8a, + 0x9e, 0x84, 0x97, 0x71, 0x0b, 0xd6, 0x27, 0xd0, 0x5d, 0x63, 0x05, 0x37, 0x80, 0x19, 0x52, 0xde, + 0x3d, 0x15, 0x70, 0x1f, 0x76, 0xfe, 0xef, 0x7d, 0x17, 0x58, 0xcd, 0xa3, 0x99, 0x59, 0xd2, 0x8d, + 0x03, 0x64, 0xb5, 0xf9, 0x36, 0x17, 0x22, 0x88, 0x94, 0x65, 0xcf, 0xf1, 0x35, 0xec, 0xce, 0xda, + 0x61, 0x74, 0xd8, 0x95, 0xc2, 0xe5, 0x77, 0x61, 0x75, 0xdc, 0x83, 0xed, 0xc9, 0x3d, 0x44, 0xe0, + 0x91, 0xe3, 0xde, 0x77, 0xd2, 0x56, 0x1a, 0xf2, 0x49, 0x59, 0xf6, 0x02, 0x5b, 0xb0, 0x17, 0x46, + 0xa6, 0xe3, 0x54, 0x60, 0xe5, 0x91, 0x14, 0xa3, 0x16, 0x9a, 0x8e, 0xa5, 0xb1, 0x7a, 0x14, 0x39, + 0xcb, 0x13, 0x7a, 0x9c, 0x71, 0x9a, 0x4c, 0x18, 0x28, 0x43, 0x6c, 0x0d, 0x77, 0x60, 0x6b, 0x16, + 0x3e, 0x89, 0x48, 0x9f, 0x32, 0xc4, 0xb7, 0xd0, 0x7c, 0xc0, 0x9c, 0xb6, 0x58, 0xcf, 0xb7, 0x9e, + 0x37, 0xaf, 0xc8, 0x8f, 0x6d, 0xe4, 0x2b, 0xcd, 0xb3, 0xc7, 0xe5, 0x9b, 0x67, 0x95, 0xe2, 0xd7, + 0xfe, 0xf2, 0x37, 0x00, 0x00, 0xff, 0xff, 0xe7, 0xcd, 0xf5, 0xf1, 0x77, 0x03, 0x00, 0x00, } diff --git a/protocol/protobuf/application_metadata_message.proto b/protocol/protobuf/application_metadata_message.proto index dae1d78be..705be609e 100644 --- a/protocol/protobuf/application_metadata_message.proto +++ b/protocol/protobuf/application_metadata_message.proto @@ -27,5 +27,12 @@ message ApplicationMetadataMessage { SYNC_INSTALLATION_CONTACT = 12; SYNC_INSTALLATION_ACCOUNT = 13; SYNC_INSTALLATION_PUBLIC_CHAT = 14; + CONTACT_CODE_ADVERTISEMENT = 15; + PUSH_NOTIFICATION_REGISTRATION = 16; + PUSH_NOTIFICATION_REGISTRATION_RESPONSE = 17; + PUSH_NOTIFICATION_QUERY = 18; + PUSH_NOTIFICATION_QUERY_RESPONSE = 19; + PUSH_NOTIFICATION_REQUEST = 20; + PUSH_NOTIFICATION_RESPONSE = 21; } } diff --git a/protocol/protobuf/push_notifications.pb.go b/protocol/protobuf/push_notifications.pb.go new file mode 100644 index 000000000..02dd42b56 --- /dev/null +++ b/protocol/protobuf/push_notifications.pb.go @@ -0,0 +1,813 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: push_notifications.proto + +package protobuf + +import ( + fmt "fmt" + proto "github.com/golang/protobuf/proto" + math "math" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package + +type PushNotificationRegistration_TokenType int32 + +const ( + PushNotificationRegistration_UNKNOWN_TOKEN_TYPE PushNotificationRegistration_TokenType = 0 + PushNotificationRegistration_APN_TOKEN PushNotificationRegistration_TokenType = 1 + PushNotificationRegistration_FIREBASE_TOKEN PushNotificationRegistration_TokenType = 2 +) + +var PushNotificationRegistration_TokenType_name = map[int32]string{ + 0: "UNKNOWN_TOKEN_TYPE", + 1: "APN_TOKEN", + 2: "FIREBASE_TOKEN", +} + +var PushNotificationRegistration_TokenType_value = map[string]int32{ + "UNKNOWN_TOKEN_TYPE": 0, + "APN_TOKEN": 1, + "FIREBASE_TOKEN": 2, +} + +func (x PushNotificationRegistration_TokenType) String() string { + return proto.EnumName(PushNotificationRegistration_TokenType_name, int32(x)) +} + +func (PushNotificationRegistration_TokenType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{0, 0} +} + +type PushNotificationRegistrationResponse_ErrorType int32 + +const ( + PushNotificationRegistrationResponse_UNKNOWN_ERROR_TYPE PushNotificationRegistrationResponse_ErrorType = 0 + PushNotificationRegistrationResponse_MALFORMED_MESSAGE PushNotificationRegistrationResponse_ErrorType = 1 + PushNotificationRegistrationResponse_VERSION_MISMATCH PushNotificationRegistrationResponse_ErrorType = 2 + PushNotificationRegistrationResponse_UNSUPPORTED_TOKEN_TYPE PushNotificationRegistrationResponse_ErrorType = 3 + PushNotificationRegistrationResponse_INTERNAL_ERROR PushNotificationRegistrationResponse_ErrorType = 4 +) + +var PushNotificationRegistrationResponse_ErrorType_name = map[int32]string{ + 0: "UNKNOWN_ERROR_TYPE", + 1: "MALFORMED_MESSAGE", + 2: "VERSION_MISMATCH", + 3: "UNSUPPORTED_TOKEN_TYPE", + 4: "INTERNAL_ERROR", +} + +var PushNotificationRegistrationResponse_ErrorType_value = map[string]int32{ + "UNKNOWN_ERROR_TYPE": 0, + "MALFORMED_MESSAGE": 1, + "VERSION_MISMATCH": 2, + "UNSUPPORTED_TOKEN_TYPE": 3, + "INTERNAL_ERROR": 4, +} + +func (x PushNotificationRegistrationResponse_ErrorType) String() string { + return proto.EnumName(PushNotificationRegistrationResponse_ErrorType_name, int32(x)) +} + +func (PushNotificationRegistrationResponse_ErrorType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{1, 0} +} + +type PushNotificationReport_ErrorType int32 + +const ( + PushNotificationReport_UNKNOWN_ERROR_TYPE PushNotificationReport_ErrorType = 0 + PushNotificationReport_WRONG_TOKEN PushNotificationReport_ErrorType = 1 + PushNotificationReport_INTERNAL_ERROR PushNotificationReport_ErrorType = 2 + PushNotificationReport_NOT_REGISTERED PushNotificationReport_ErrorType = 3 +) + +var PushNotificationReport_ErrorType_name = map[int32]string{ + 0: "UNKNOWN_ERROR_TYPE", + 1: "WRONG_TOKEN", + 2: "INTERNAL_ERROR", + 3: "NOT_REGISTERED", +} + +var PushNotificationReport_ErrorType_value = map[string]int32{ + "UNKNOWN_ERROR_TYPE": 0, + "WRONG_TOKEN": 1, + "INTERNAL_ERROR": 2, + "NOT_REGISTERED": 3, +} + +func (x PushNotificationReport_ErrorType) String() string { + return proto.EnumName(PushNotificationReport_ErrorType_name, int32(x)) +} + +func (PushNotificationReport_ErrorType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{8, 0} +} + +type PushNotificationRegistration struct { + TokenType PushNotificationRegistration_TokenType `protobuf:"varint,1,opt,name=token_type,json=tokenType,proto3,enum=protobuf.PushNotificationRegistration_TokenType" json:"token_type,omitempty"` + DeviceToken string `protobuf:"bytes,2,opt,name=device_token,json=deviceToken,proto3" json:"device_token,omitempty"` + InstallationId string `protobuf:"bytes,3,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + AccessToken string `protobuf:"bytes,4,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"` + Version uint64 `protobuf:"varint,6,opt,name=version,proto3" json:"version,omitempty"` + AllowedKeyList [][]byte `protobuf:"bytes,7,rep,name=allowed_key_list,json=allowedKeyList,proto3" json:"allowed_key_list,omitempty"` + BlockedChatList [][]byte `protobuf:"bytes,8,rep,name=blocked_chat_list,json=blockedChatList,proto3" json:"blocked_chat_list,omitempty"` + Unregister bool `protobuf:"varint,9,opt,name=unregister,proto3" json:"unregister,omitempty"` + Grant []byte `protobuf:"bytes,10,opt,name=grant,proto3" json:"grant,omitempty"` + AllowFromContactsOnly bool `protobuf:"varint,11,opt,name=allow_from_contacts_only,json=allowFromContactsOnly,proto3" json:"allow_from_contacts_only,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PushNotificationRegistration) Reset() { *m = PushNotificationRegistration{} } +func (m *PushNotificationRegistration) String() string { return proto.CompactTextString(m) } +func (*PushNotificationRegistration) ProtoMessage() {} +func (*PushNotificationRegistration) Descriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{0} +} + +func (m *PushNotificationRegistration) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PushNotificationRegistration.Unmarshal(m, b) +} +func (m *PushNotificationRegistration) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PushNotificationRegistration.Marshal(b, m, deterministic) +} +func (m *PushNotificationRegistration) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushNotificationRegistration.Merge(m, src) +} +func (m *PushNotificationRegistration) XXX_Size() int { + return xxx_messageInfo_PushNotificationRegistration.Size(m) +} +func (m *PushNotificationRegistration) XXX_DiscardUnknown() { + xxx_messageInfo_PushNotificationRegistration.DiscardUnknown(m) +} + +var xxx_messageInfo_PushNotificationRegistration proto.InternalMessageInfo + +func (m *PushNotificationRegistration) GetTokenType() PushNotificationRegistration_TokenType { + if m != nil { + return m.TokenType + } + return PushNotificationRegistration_UNKNOWN_TOKEN_TYPE +} + +func (m *PushNotificationRegistration) GetDeviceToken() string { + if m != nil { + return m.DeviceToken + } + return "" +} + +func (m *PushNotificationRegistration) GetInstallationId() string { + if m != nil { + return m.InstallationId + } + return "" +} + +func (m *PushNotificationRegistration) GetAccessToken() string { + if m != nil { + return m.AccessToken + } + return "" +} + +func (m *PushNotificationRegistration) GetEnabled() bool { + if m != nil { + return m.Enabled + } + return false +} + +func (m *PushNotificationRegistration) GetVersion() uint64 { + if m != nil { + return m.Version + } + return 0 +} + +func (m *PushNotificationRegistration) GetAllowedKeyList() [][]byte { + if m != nil { + return m.AllowedKeyList + } + return nil +} + +func (m *PushNotificationRegistration) GetBlockedChatList() [][]byte { + if m != nil { + return m.BlockedChatList + } + return nil +} + +func (m *PushNotificationRegistration) GetUnregister() bool { + if m != nil { + return m.Unregister + } + return false +} + +func (m *PushNotificationRegistration) GetGrant() []byte { + if m != nil { + return m.Grant + } + return nil +} + +func (m *PushNotificationRegistration) GetAllowFromContactsOnly() bool { + if m != nil { + return m.AllowFromContactsOnly + } + return false +} + +type PushNotificationRegistrationResponse struct { + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + Error PushNotificationRegistrationResponse_ErrorType `protobuf:"varint,2,opt,name=error,proto3,enum=protobuf.PushNotificationRegistrationResponse_ErrorType" json:"error,omitempty"` + RequestId []byte `protobuf:"bytes,3,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PushNotificationRegistrationResponse) Reset() { *m = PushNotificationRegistrationResponse{} } +func (m *PushNotificationRegistrationResponse) String() string { return proto.CompactTextString(m) } +func (*PushNotificationRegistrationResponse) ProtoMessage() {} +func (*PushNotificationRegistrationResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{1} +} + +func (m *PushNotificationRegistrationResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PushNotificationRegistrationResponse.Unmarshal(m, b) +} +func (m *PushNotificationRegistrationResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PushNotificationRegistrationResponse.Marshal(b, m, deterministic) +} +func (m *PushNotificationRegistrationResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushNotificationRegistrationResponse.Merge(m, src) +} +func (m *PushNotificationRegistrationResponse) XXX_Size() int { + return xxx_messageInfo_PushNotificationRegistrationResponse.Size(m) +} +func (m *PushNotificationRegistrationResponse) XXX_DiscardUnknown() { + xxx_messageInfo_PushNotificationRegistrationResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_PushNotificationRegistrationResponse proto.InternalMessageInfo + +func (m *PushNotificationRegistrationResponse) GetSuccess() bool { + if m != nil { + return m.Success + } + return false +} + +func (m *PushNotificationRegistrationResponse) GetError() PushNotificationRegistrationResponse_ErrorType { + if m != nil { + return m.Error + } + return PushNotificationRegistrationResponse_UNKNOWN_ERROR_TYPE +} + +func (m *PushNotificationRegistrationResponse) GetRequestId() []byte { + if m != nil { + return m.RequestId + } + return nil +} + +type ContactCodeAdvertisement struct { + PushNotificationInfo []*PushNotificationQueryInfo `protobuf:"bytes,1,rep,name=push_notification_info,json=pushNotificationInfo,proto3" json:"push_notification_info,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ContactCodeAdvertisement) Reset() { *m = ContactCodeAdvertisement{} } +func (m *ContactCodeAdvertisement) String() string { return proto.CompactTextString(m) } +func (*ContactCodeAdvertisement) ProtoMessage() {} +func (*ContactCodeAdvertisement) Descriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{2} +} + +func (m *ContactCodeAdvertisement) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ContactCodeAdvertisement.Unmarshal(m, b) +} +func (m *ContactCodeAdvertisement) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ContactCodeAdvertisement.Marshal(b, m, deterministic) +} +func (m *ContactCodeAdvertisement) XXX_Merge(src proto.Message) { + xxx_messageInfo_ContactCodeAdvertisement.Merge(m, src) +} +func (m *ContactCodeAdvertisement) XXX_Size() int { + return xxx_messageInfo_ContactCodeAdvertisement.Size(m) +} +func (m *ContactCodeAdvertisement) XXX_DiscardUnknown() { + xxx_messageInfo_ContactCodeAdvertisement.DiscardUnknown(m) +} + +var xxx_messageInfo_ContactCodeAdvertisement proto.InternalMessageInfo + +func (m *ContactCodeAdvertisement) GetPushNotificationInfo() []*PushNotificationQueryInfo { + if m != nil { + return m.PushNotificationInfo + } + return nil +} + +type PushNotificationQuery struct { + PublicKeys [][]byte `protobuf:"bytes,1,rep,name=public_keys,json=publicKeys,proto3" json:"public_keys,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PushNotificationQuery) Reset() { *m = PushNotificationQuery{} } +func (m *PushNotificationQuery) String() string { return proto.CompactTextString(m) } +func (*PushNotificationQuery) ProtoMessage() {} +func (*PushNotificationQuery) Descriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{3} +} + +func (m *PushNotificationQuery) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PushNotificationQuery.Unmarshal(m, b) +} +func (m *PushNotificationQuery) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PushNotificationQuery.Marshal(b, m, deterministic) +} +func (m *PushNotificationQuery) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushNotificationQuery.Merge(m, src) +} +func (m *PushNotificationQuery) XXX_Size() int { + return xxx_messageInfo_PushNotificationQuery.Size(m) +} +func (m *PushNotificationQuery) XXX_DiscardUnknown() { + xxx_messageInfo_PushNotificationQuery.DiscardUnknown(m) +} + +var xxx_messageInfo_PushNotificationQuery proto.InternalMessageInfo + +func (m *PushNotificationQuery) GetPublicKeys() [][]byte { + if m != nil { + return m.PublicKeys + } + return nil +} + +type PushNotificationQueryInfo struct { + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + InstallationId string `protobuf:"bytes,2,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + PublicKey []byte `protobuf:"bytes,3,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + AllowedKeyList [][]byte `protobuf:"bytes,4,rep,name=allowed_key_list,json=allowedKeyList,proto3" json:"allowed_key_list,omitempty"` + Grant []byte `protobuf:"bytes,5,opt,name=grant,proto3" json:"grant,omitempty"` + Version uint64 `protobuf:"varint,6,opt,name=version,proto3" json:"version,omitempty"` + ServerPublicKey []byte `protobuf:"bytes,7,opt,name=server_public_key,json=serverPublicKey,proto3" json:"server_public_key,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PushNotificationQueryInfo) Reset() { *m = PushNotificationQueryInfo{} } +func (m *PushNotificationQueryInfo) String() string { return proto.CompactTextString(m) } +func (*PushNotificationQueryInfo) ProtoMessage() {} +func (*PushNotificationQueryInfo) Descriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{4} +} + +func (m *PushNotificationQueryInfo) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PushNotificationQueryInfo.Unmarshal(m, b) +} +func (m *PushNotificationQueryInfo) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PushNotificationQueryInfo.Marshal(b, m, deterministic) +} +func (m *PushNotificationQueryInfo) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushNotificationQueryInfo.Merge(m, src) +} +func (m *PushNotificationQueryInfo) XXX_Size() int { + return xxx_messageInfo_PushNotificationQueryInfo.Size(m) +} +func (m *PushNotificationQueryInfo) XXX_DiscardUnknown() { + xxx_messageInfo_PushNotificationQueryInfo.DiscardUnknown(m) +} + +var xxx_messageInfo_PushNotificationQueryInfo proto.InternalMessageInfo + +func (m *PushNotificationQueryInfo) GetAccessToken() string { + if m != nil { + return m.AccessToken + } + return "" +} + +func (m *PushNotificationQueryInfo) GetInstallationId() string { + if m != nil { + return m.InstallationId + } + return "" +} + +func (m *PushNotificationQueryInfo) GetPublicKey() []byte { + if m != nil { + return m.PublicKey + } + return nil +} + +func (m *PushNotificationQueryInfo) GetAllowedKeyList() [][]byte { + if m != nil { + return m.AllowedKeyList + } + return nil +} + +func (m *PushNotificationQueryInfo) GetGrant() []byte { + if m != nil { + return m.Grant + } + return nil +} + +func (m *PushNotificationQueryInfo) GetVersion() uint64 { + if m != nil { + return m.Version + } + return 0 +} + +func (m *PushNotificationQueryInfo) GetServerPublicKey() []byte { + if m != nil { + return m.ServerPublicKey + } + return nil +} + +type PushNotificationQueryResponse struct { + Info []*PushNotificationQueryInfo `protobuf:"bytes,1,rep,name=info,proto3" json:"info,omitempty"` + MessageId []byte `protobuf:"bytes,2,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + Success bool `protobuf:"varint,3,opt,name=success,proto3" json:"success,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PushNotificationQueryResponse) Reset() { *m = PushNotificationQueryResponse{} } +func (m *PushNotificationQueryResponse) String() string { return proto.CompactTextString(m) } +func (*PushNotificationQueryResponse) ProtoMessage() {} +func (*PushNotificationQueryResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{5} +} + +func (m *PushNotificationQueryResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PushNotificationQueryResponse.Unmarshal(m, b) +} +func (m *PushNotificationQueryResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PushNotificationQueryResponse.Marshal(b, m, deterministic) +} +func (m *PushNotificationQueryResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushNotificationQueryResponse.Merge(m, src) +} +func (m *PushNotificationQueryResponse) XXX_Size() int { + return xxx_messageInfo_PushNotificationQueryResponse.Size(m) +} +func (m *PushNotificationQueryResponse) XXX_DiscardUnknown() { + xxx_messageInfo_PushNotificationQueryResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_PushNotificationQueryResponse proto.InternalMessageInfo + +func (m *PushNotificationQueryResponse) GetInfo() []*PushNotificationQueryInfo { + if m != nil { + return m.Info + } + return nil +} + +func (m *PushNotificationQueryResponse) GetMessageId() []byte { + if m != nil { + return m.MessageId + } + return nil +} + +func (m *PushNotificationQueryResponse) GetSuccess() bool { + if m != nil { + return m.Success + } + return false +} + +type PushNotification struct { + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + ChatId string `protobuf:"bytes,2,opt,name=chat_id,json=chatId,proto3" json:"chat_id,omitempty"` + PublicKey []byte `protobuf:"bytes,3,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + InstallationId string `protobuf:"bytes,4,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + Message []byte `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PushNotification) Reset() { *m = PushNotification{} } +func (m *PushNotification) String() string { return proto.CompactTextString(m) } +func (*PushNotification) ProtoMessage() {} +func (*PushNotification) Descriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{6} +} + +func (m *PushNotification) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PushNotification.Unmarshal(m, b) +} +func (m *PushNotification) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PushNotification.Marshal(b, m, deterministic) +} +func (m *PushNotification) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushNotification.Merge(m, src) +} +func (m *PushNotification) XXX_Size() int { + return xxx_messageInfo_PushNotification.Size(m) +} +func (m *PushNotification) XXX_DiscardUnknown() { + xxx_messageInfo_PushNotification.DiscardUnknown(m) +} + +var xxx_messageInfo_PushNotification proto.InternalMessageInfo + +func (m *PushNotification) GetAccessToken() string { + if m != nil { + return m.AccessToken + } + return "" +} + +func (m *PushNotification) GetChatId() string { + if m != nil { + return m.ChatId + } + return "" +} + +func (m *PushNotification) GetPublicKey() []byte { + if m != nil { + return m.PublicKey + } + return nil +} + +func (m *PushNotification) GetInstallationId() string { + if m != nil { + return m.InstallationId + } + return "" +} + +func (m *PushNotification) GetMessage() []byte { + if m != nil { + return m.Message + } + return nil +} + +type PushNotificationRequest struct { + Requests []*PushNotification `protobuf:"bytes,1,rep,name=requests,proto3" json:"requests,omitempty"` + MessageId []byte `protobuf:"bytes,2,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PushNotificationRequest) Reset() { *m = PushNotificationRequest{} } +func (m *PushNotificationRequest) String() string { return proto.CompactTextString(m) } +func (*PushNotificationRequest) ProtoMessage() {} +func (*PushNotificationRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{7} +} + +func (m *PushNotificationRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PushNotificationRequest.Unmarshal(m, b) +} +func (m *PushNotificationRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PushNotificationRequest.Marshal(b, m, deterministic) +} +func (m *PushNotificationRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushNotificationRequest.Merge(m, src) +} +func (m *PushNotificationRequest) XXX_Size() int { + return xxx_messageInfo_PushNotificationRequest.Size(m) +} +func (m *PushNotificationRequest) XXX_DiscardUnknown() { + xxx_messageInfo_PushNotificationRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_PushNotificationRequest proto.InternalMessageInfo + +func (m *PushNotificationRequest) GetRequests() []*PushNotification { + if m != nil { + return m.Requests + } + return nil +} + +func (m *PushNotificationRequest) GetMessageId() []byte { + if m != nil { + return m.MessageId + } + return nil +} + +type PushNotificationReport struct { + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + Error PushNotificationReport_ErrorType `protobuf:"varint,2,opt,name=error,proto3,enum=protobuf.PushNotificationReport_ErrorType" json:"error,omitempty"` + PublicKey []byte `protobuf:"bytes,3,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` + InstallationId string `protobuf:"bytes,4,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PushNotificationReport) Reset() { *m = PushNotificationReport{} } +func (m *PushNotificationReport) String() string { return proto.CompactTextString(m) } +func (*PushNotificationReport) ProtoMessage() {} +func (*PushNotificationReport) Descriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{8} +} + +func (m *PushNotificationReport) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PushNotificationReport.Unmarshal(m, b) +} +func (m *PushNotificationReport) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PushNotificationReport.Marshal(b, m, deterministic) +} +func (m *PushNotificationReport) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushNotificationReport.Merge(m, src) +} +func (m *PushNotificationReport) XXX_Size() int { + return xxx_messageInfo_PushNotificationReport.Size(m) +} +func (m *PushNotificationReport) XXX_DiscardUnknown() { + xxx_messageInfo_PushNotificationReport.DiscardUnknown(m) +} + +var xxx_messageInfo_PushNotificationReport proto.InternalMessageInfo + +func (m *PushNotificationReport) GetSuccess() bool { + if m != nil { + return m.Success + } + return false +} + +func (m *PushNotificationReport) GetError() PushNotificationReport_ErrorType { + if m != nil { + return m.Error + } + return PushNotificationReport_UNKNOWN_ERROR_TYPE +} + +func (m *PushNotificationReport) GetPublicKey() []byte { + if m != nil { + return m.PublicKey + } + return nil +} + +func (m *PushNotificationReport) GetInstallationId() string { + if m != nil { + return m.InstallationId + } + return "" +} + +type PushNotificationResponse struct { + MessageId []byte `protobuf:"bytes,1,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + Reports []*PushNotificationReport `protobuf:"bytes,2,rep,name=reports,proto3" json:"reports,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PushNotificationResponse) Reset() { *m = PushNotificationResponse{} } +func (m *PushNotificationResponse) String() string { return proto.CompactTextString(m) } +func (*PushNotificationResponse) ProtoMessage() {} +func (*PushNotificationResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_200acd86044eaa5d, []int{9} +} + +func (m *PushNotificationResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PushNotificationResponse.Unmarshal(m, b) +} +func (m *PushNotificationResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PushNotificationResponse.Marshal(b, m, deterministic) +} +func (m *PushNotificationResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_PushNotificationResponse.Merge(m, src) +} +func (m *PushNotificationResponse) XXX_Size() int { + return xxx_messageInfo_PushNotificationResponse.Size(m) +} +func (m *PushNotificationResponse) XXX_DiscardUnknown() { + xxx_messageInfo_PushNotificationResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_PushNotificationResponse proto.InternalMessageInfo + +func (m *PushNotificationResponse) GetMessageId() []byte { + if m != nil { + return m.MessageId + } + return nil +} + +func (m *PushNotificationResponse) GetReports() []*PushNotificationReport { + if m != nil { + return m.Reports + } + return nil +} + +func init() { + proto.RegisterEnum("protobuf.PushNotificationRegistration_TokenType", PushNotificationRegistration_TokenType_name, PushNotificationRegistration_TokenType_value) + proto.RegisterEnum("protobuf.PushNotificationRegistrationResponse_ErrorType", PushNotificationRegistrationResponse_ErrorType_name, PushNotificationRegistrationResponse_ErrorType_value) + proto.RegisterEnum("protobuf.PushNotificationReport_ErrorType", PushNotificationReport_ErrorType_name, PushNotificationReport_ErrorType_value) + proto.RegisterType((*PushNotificationRegistration)(nil), "protobuf.PushNotificationRegistration") + proto.RegisterType((*PushNotificationRegistrationResponse)(nil), "protobuf.PushNotificationRegistrationResponse") + proto.RegisterType((*ContactCodeAdvertisement)(nil), "protobuf.ContactCodeAdvertisement") + proto.RegisterType((*PushNotificationQuery)(nil), "protobuf.PushNotificationQuery") + proto.RegisterType((*PushNotificationQueryInfo)(nil), "protobuf.PushNotificationQueryInfo") + proto.RegisterType((*PushNotificationQueryResponse)(nil), "protobuf.PushNotificationQueryResponse") + proto.RegisterType((*PushNotification)(nil), "protobuf.PushNotification") + proto.RegisterType((*PushNotificationRequest)(nil), "protobuf.PushNotificationRequest") + proto.RegisterType((*PushNotificationReport)(nil), "protobuf.PushNotificationReport") + proto.RegisterType((*PushNotificationResponse)(nil), "protobuf.PushNotificationResponse") +} + +func init() { proto.RegisterFile("push_notifications.proto", fileDescriptor_200acd86044eaa5d) } + +var fileDescriptor_200acd86044eaa5d = []byte{ + // 878 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x54, 0x41, 0x6f, 0xeb, 0x44, + 0x17, 0xfd, 0x9c, 0xa4, 0x4d, 0x72, 0x93, 0x2f, 0x4d, 0x47, 0x6d, 0x9f, 0x79, 0xa2, 0x10, 0x0c, + 0x12, 0x51, 0x17, 0x11, 0x2a, 0x12, 0xef, 0x89, 0x15, 0xa1, 0x75, 0x8b, 0xd5, 0xc6, 0x0e, 0x13, + 0x97, 0xa7, 0x27, 0x21, 0x59, 0x8e, 0x3d, 0x69, 0xad, 0xba, 0x1e, 0x33, 0x33, 0x2e, 0xca, 0x8e, + 0x1f, 0xc0, 0x86, 0x2d, 0x1b, 0xfe, 0x02, 0xe2, 0x17, 0x22, 0x8f, 0xed, 0xe0, 0x36, 0x6e, 0x5a, + 0x24, 0x56, 0xf6, 0x9c, 0xb9, 0xf7, 0xce, 0xcc, 0x39, 0xf7, 0x5c, 0x50, 0xe3, 0x84, 0xdf, 0x38, + 0x11, 0x15, 0xc1, 0x22, 0xf0, 0x5c, 0x11, 0xd0, 0x88, 0x8f, 0x62, 0x46, 0x05, 0x45, 0x2d, 0xf9, + 0x99, 0x27, 0x0b, 0xed, 0x8f, 0x06, 0x7c, 0x38, 0x4d, 0xf8, 0x8d, 0x59, 0x8a, 0xc2, 0xe4, 0x3a, + 0xe0, 0x82, 0xc9, 0x7f, 0x64, 0x01, 0x08, 0x7a, 0x4b, 0x22, 0x47, 0x2c, 0x63, 0xa2, 0x2a, 0x03, + 0x65, 0xd8, 0x3b, 0xfe, 0x62, 0x54, 0xe4, 0x8f, 0x36, 0xe5, 0x8e, 0xec, 0x34, 0xd1, 0x5e, 0xc6, + 0x04, 0xb7, 0x45, 0xf1, 0x8b, 0x3e, 0x81, 0xae, 0x4f, 0xee, 0x03, 0x8f, 0x38, 0x12, 0x53, 0x6b, + 0x03, 0x65, 0xd8, 0xc6, 0x9d, 0x0c, 0x93, 0x19, 0xe8, 0x73, 0xd8, 0x09, 0x22, 0x2e, 0xdc, 0x30, + 0x94, 0x75, 0x9c, 0xc0, 0x57, 0xeb, 0x32, 0xaa, 0x57, 0x86, 0x0d, 0x3f, 0xad, 0xe5, 0x7a, 0x1e, + 0xe1, 0x3c, 0xaf, 0xd5, 0xc8, 0x6a, 0x65, 0x58, 0x56, 0x4b, 0x85, 0x26, 0x89, 0xdc, 0x79, 0x48, + 0x7c, 0x75, 0x6b, 0xa0, 0x0c, 0x5b, 0xb8, 0x58, 0xa6, 0x3b, 0xf7, 0x84, 0xf1, 0x80, 0x46, 0xea, + 0xf6, 0x40, 0x19, 0x36, 0x70, 0xb1, 0x44, 0x43, 0xe8, 0xbb, 0x61, 0x48, 0x7f, 0x26, 0xbe, 0x73, + 0x4b, 0x96, 0x4e, 0x18, 0x70, 0xa1, 0x36, 0x07, 0xf5, 0x61, 0x17, 0xf7, 0x72, 0xfc, 0x82, 0x2c, + 0x2f, 0x03, 0x2e, 0xd0, 0x11, 0xec, 0xce, 0x43, 0xea, 0xdd, 0x12, 0xdf, 0xf1, 0x6e, 0x5c, 0x91, + 0x85, 0xb6, 0x64, 0xe8, 0x4e, 0xbe, 0x71, 0x72, 0xe3, 0x0a, 0x19, 0xfb, 0x11, 0x40, 0x12, 0x31, + 0xc9, 0x0f, 0x61, 0x6a, 0x5b, 0x5e, 0xa6, 0x84, 0xa0, 0x3d, 0xd8, 0xba, 0x66, 0x6e, 0x24, 0x54, + 0x18, 0x28, 0xc3, 0x2e, 0xce, 0x16, 0xe8, 0x0d, 0xa8, 0xf2, 0x4c, 0x67, 0xc1, 0xe8, 0x9d, 0xe3, + 0xd1, 0x48, 0xb8, 0x9e, 0xe0, 0x0e, 0x8d, 0xc2, 0xa5, 0xda, 0x91, 0x35, 0xf6, 0xe5, 0xfe, 0x19, + 0xa3, 0x77, 0x27, 0xf9, 0xae, 0x15, 0x85, 0x4b, 0xed, 0x0c, 0xda, 0x2b, 0xfe, 0xd1, 0x01, 0xa0, + 0x2b, 0xf3, 0xc2, 0xb4, 0xde, 0x99, 0x8e, 0x6d, 0x5d, 0xe8, 0xa6, 0x63, 0xbf, 0x9f, 0xea, 0xfd, + 0xff, 0xa1, 0xff, 0x43, 0x7b, 0x3c, 0xcd, 0xb1, 0xbe, 0x82, 0x10, 0xf4, 0xce, 0x0c, 0xac, 0x7f, + 0x3b, 0x9e, 0xe9, 0x39, 0x56, 0xd3, 0xfe, 0xaa, 0xc1, 0x67, 0x9b, 0x54, 0xc6, 0x84, 0xc7, 0x34, + 0xe2, 0x24, 0xe5, 0x93, 0x27, 0x92, 0x79, 0xd9, 0x26, 0x2d, 0x5c, 0x2c, 0x91, 0x09, 0x5b, 0x84, + 0x31, 0xca, 0xa4, 0xd6, 0xbd, 0xe3, 0xb7, 0x2f, 0x6b, 0x9f, 0xa2, 0xf0, 0x48, 0x4f, 0x73, 0x65, + 0x1b, 0x65, 0x65, 0xd0, 0x21, 0x00, 0x23, 0x3f, 0x25, 0x84, 0x8b, 0xa2, 0x35, 0xba, 0xb8, 0x9d, + 0x23, 0x86, 0xaf, 0xfd, 0xa2, 0x40, 0x7b, 0x95, 0x53, 0x7e, 0xba, 0x8e, 0xb1, 0x85, 0x8b, 0xa7, + 0xef, 0xc3, 0xee, 0x64, 0x7c, 0x79, 0x66, 0xe1, 0x89, 0x7e, 0xea, 0x4c, 0xf4, 0xd9, 0x6c, 0x7c, + 0xae, 0xf7, 0x15, 0xb4, 0x07, 0xfd, 0x1f, 0x74, 0x3c, 0x33, 0x2c, 0xd3, 0x99, 0x18, 0xb3, 0xc9, + 0xd8, 0x3e, 0xf9, 0xae, 0x5f, 0x43, 0xaf, 0xe1, 0xe0, 0xca, 0x9c, 0x5d, 0x4d, 0xa7, 0x16, 0xb6, + 0xf5, 0xd3, 0x32, 0x87, 0xf5, 0x94, 0x34, 0xc3, 0xb4, 0x75, 0x6c, 0x8e, 0x2f, 0xb3, 0x13, 0xfa, + 0x0d, 0x2d, 0x01, 0x35, 0x17, 0xe3, 0x84, 0xfa, 0x64, 0xec, 0xdf, 0x13, 0x26, 0x02, 0x4e, 0xee, + 0x48, 0x24, 0xd0, 0x7b, 0x38, 0x58, 0x33, 0xa6, 0x13, 0x44, 0x0b, 0xaa, 0x2a, 0x83, 0xfa, 0xb0, + 0x73, 0xfc, 0xe9, 0xd3, 0xf4, 0x7c, 0x9f, 0x10, 0xb6, 0x34, 0xa2, 0x05, 0xc5, 0x7b, 0xf1, 0xa3, + 0xad, 0x14, 0xd5, 0xde, 0xc2, 0x7e, 0x65, 0x0a, 0xfa, 0x18, 0x3a, 0x71, 0x32, 0x0f, 0x03, 0x2f, + 0x6d, 0x68, 0x2e, 0x0f, 0xea, 0x62, 0xc8, 0xa0, 0x0b, 0xb2, 0xe4, 0xda, 0xaf, 0x35, 0xf8, 0xe0, + 0xc9, 0xd3, 0xd6, 0x7c, 0xa6, 0xac, 0xfb, 0xac, 0xc2, 0xb3, 0xb5, 0x4a, 0xcf, 0x1e, 0x02, 0xfc, + 0x73, 0x95, 0x42, 0xbc, 0xd5, 0x4d, 0x2a, 0xbd, 0xd7, 0xa8, 0xf4, 0xde, 0xca, 0x2f, 0x5b, 0x65, + 0xbf, 0x3c, 0xed, 0xea, 0x23, 0xd8, 0xe5, 0x84, 0xdd, 0x13, 0xe6, 0x94, 0xce, 0x6f, 0xca, 0xdc, + 0x9d, 0x6c, 0x63, 0x5a, 0xdc, 0x42, 0xfb, 0x4d, 0x81, 0xc3, 0x4a, 0x3a, 0x56, 0xdd, 0xfe, 0x06, + 0x1a, 0xff, 0x56, 0x33, 0x99, 0x90, 0xbe, 0xff, 0x8e, 0x70, 0xee, 0x5e, 0x93, 0x82, 0xa3, 0x2e, + 0x6e, 0xe7, 0x88, 0xe1, 0x97, 0x5d, 0x54, 0x7f, 0xe0, 0x22, 0xed, 0x4f, 0x05, 0xfa, 0x8f, 0x8b, + 0xbf, 0x44, 0x99, 0x57, 0xd0, 0x94, 0xb3, 0x69, 0xa5, 0xc8, 0x76, 0xba, 0x7c, 0x5e, 0x89, 0x0a, + 0x45, 0x1b, 0x95, 0x8a, 0xaa, 0xd0, 0xcc, 0xef, 0x9f, 0x4b, 0x51, 0x2c, 0xb5, 0x18, 0x5e, 0xad, + 0x3b, 0x5c, 0xda, 0x14, 0x7d, 0x05, 0xad, 0xdc, 0xb1, 0x3c, 0xe7, 0xf0, 0xf5, 0x86, 0xb1, 0xb0, + 0x8a, 0x7d, 0x86, 0x3e, 0xed, 0xf7, 0x1a, 0x1c, 0xac, 0x1f, 0x19, 0x53, 0x26, 0x36, 0xcc, 0xa7, + 0x6f, 0x1e, 0xce, 0xa7, 0xa3, 0x4d, 0xf3, 0x29, 0x2d, 0x55, 0x39, 0x91, 0xfe, 0x0b, 0x2a, 0xb5, + 0x1f, 0x5f, 0x32, 0xb9, 0x76, 0xa0, 0xf3, 0x0e, 0x5b, 0xe6, 0x79, 0x79, 0x6c, 0x3f, 0x9a, 0x40, + 0xb5, 0x14, 0x33, 0x2d, 0xdb, 0xc1, 0xfa, 0xb9, 0x31, 0xb3, 0x75, 0xac, 0x9f, 0xf6, 0xeb, 0xe9, + 0x54, 0x5a, 0x7f, 0x50, 0xde, 0xcf, 0x0f, 0x79, 0x55, 0x1e, 0xb7, 0xe5, 0xd7, 0xd0, 0x64, 0xf2, + 0xed, 0x5c, 0xad, 0x49, 0xb5, 0x06, 0xcf, 0x91, 0x84, 0x8b, 0x84, 0xf9, 0xb6, 0x8c, 0xfc, 0xf2, + 0xef, 0x00, 0x00, 0x00, 0xff, 0xff, 0x6f, 0xe8, 0xb9, 0x16, 0x90, 0x08, 0x00, 0x00, +} diff --git a/protocol/protobuf/push_notifications.proto b/protocol/protobuf/push_notifications.proto new file mode 100644 index 000000000..ce4ec1c45 --- /dev/null +++ b/protocol/protobuf/push_notifications.proto @@ -0,0 +1,91 @@ +syntax = "proto3"; + +package protobuf; + +message PushNotificationRegistration { + enum TokenType { + UNKNOWN_TOKEN_TYPE = 0; + APN_TOKEN = 1; + FIREBASE_TOKEN = 2; + } + TokenType token_type = 1; + string device_token = 2; + string installation_id = 3; + string access_token = 4; + bool enabled = 5; + uint64 version = 6; + repeated bytes allowed_key_list = 7; + repeated bytes blocked_chat_list = 8; + bool unregister = 9; + bytes grant = 10; + bool allow_from_contacts_only = 11; +} + +message PushNotificationRegistrationResponse { + bool success = 1; + ErrorType error = 2; + bytes request_id = 3; + + enum ErrorType { + UNKNOWN_ERROR_TYPE = 0; + MALFORMED_MESSAGE = 1; + VERSION_MISMATCH = 2; + UNSUPPORTED_TOKEN_TYPE = 3; + INTERNAL_ERROR = 4; + } +} + +message ContactCodeAdvertisement { + repeated PushNotificationQueryInfo push_notification_info = 1; +} + +message PushNotificationQuery { + repeated bytes public_keys = 1; +} + +message PushNotificationQueryInfo { + string access_token = 1; + string installation_id = 2; + bytes public_key = 3; + repeated bytes allowed_key_list = 4; + bytes grant = 5; + uint64 version = 6; + bytes server_public_key = 7; +} + +message PushNotificationQueryResponse { + repeated PushNotificationQueryInfo info = 1; + bytes message_id = 2; + bool success = 3; +} + +message PushNotification { + string access_token = 1; + string chat_id = 2; + bytes public_key = 3; + string installation_id = 4; + bytes message = 5; +} + +message PushNotificationRequest { + repeated PushNotification requests = 1; + bytes message_id = 2; +} + +message PushNotificationReport { + bool success = 1; + ErrorType error = 2; + enum ErrorType { + UNKNOWN_ERROR_TYPE = 0; + WRONG_TOKEN = 1; + INTERNAL_ERROR = 2; + NOT_REGISTERED = 3; + } + bytes public_key = 3; + string installation_id = 4; +} + +message PushNotificationResponse { + bytes message_id = 1; + repeated PushNotificationReport reports = 2; +} diff --git a/protocol/protobuf/service.go b/protocol/protobuf/service.go index f6652e485..12d13e972 100644 --- a/protocol/protobuf/service.go +++ b/protocol/protobuf/service.go @@ -4,7 +4,7 @@ import ( "github.com/golang/protobuf/proto" ) -//go:generate protoc --go_out=. ./chat_message.proto ./application_metadata_message.proto ./membership_update_message.proto ./command.proto ./contact.proto ./pairing.proto +//go:generate protoc --go_out=. ./chat_message.proto ./application_metadata_message.proto ./membership_update_message.proto ./command.proto ./contact.proto ./pairing.proto ./push_notifications.proto func Unmarshal(payload []byte) (*ApplicationMetadataMessage, error) { var message ApplicationMetadataMessage diff --git a/protocol/push_notification_test.go b/protocol/push_notification_test.go new file mode 100644 index 000000000..35e950016 --- /dev/null +++ b/protocol/push_notification_test.go @@ -0,0 +1,797 @@ +package protocol + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "errors" + "io/ioutil" + "os" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" + "go.uber.org/zap" + + gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/pushnotificationclient" + "github.com/status-im/status-go/protocol/pushnotificationserver" + "github.com/status-im/status-go/protocol/tt" + "github.com/status-im/status-go/waku" +) + +const ( + bob1DeviceToken = "token-1" + bob2DeviceToken = "token-2" +) + +func TestMessengerPushNotificationSuite(t *testing.T) { + suite.Run(t, new(MessengerPushNotificationSuite)) +} + +type MessengerPushNotificationSuite struct { + suite.Suite + m *Messenger // main instance of Messenger + privateKey *ecdsa.PrivateKey // private key for the main instance of Messenger + // If one wants to send messages between different instances of Messenger, + // a single Waku service should be shared. + shh types.Waku + tmpFiles []*os.File // files to clean up + logger *zap.Logger +} + +func (s *MessengerPushNotificationSuite) SetupTest() { + s.logger = tt.MustCreateTestLogger() + + config := waku.DefaultConfig + config.MinimumAcceptedPoW = 0 + shh := waku.New(&config, s.logger) + s.shh = gethbridge.NewGethWakuWrapper(shh) + s.Require().NoError(shh.Start(nil)) + + s.m = s.newMessenger(s.shh) + s.privateKey = s.m.identity +} + +func (s *MessengerPushNotificationSuite) TearDownTest() { + s.Require().NoError(s.m.Shutdown()) + for _, f := range s.tmpFiles { + _ = os.Remove(f.Name()) + } + _ = s.logger.Sync() +} + +func (s *MessengerPushNotificationSuite) newMessengerWithOptions(shh types.Waku, privateKey *ecdsa.PrivateKey, options []Option) *Messenger { + m, err := NewMessenger( + privateKey, + &testNode{shh: shh}, + uuid.New().String(), + options..., + ) + s.Require().NoError(err) + + err = m.Init() + s.Require().NoError(err) + + return m +} + +func (s *MessengerPushNotificationSuite) newMessengerWithKey(shh types.Waku, privateKey *ecdsa.PrivateKey) *Messenger { + tmpFile, err := ioutil.TempFile("", "") + s.Require().NoError(err) + + options := []Option{ + WithCustomLogger(s.logger), + WithMessagesPersistenceEnabled(), + WithDatabaseConfig(tmpFile.Name(), ""), + WithDatasync(), + } + return s.newMessengerWithOptions(shh, privateKey, options) +} + +func (s *MessengerPushNotificationSuite) newMessenger(shh types.Waku) *Messenger { + privateKey, err := crypto.GenerateKey() + s.Require().NoError(err) + + return s.newMessengerWithKey(s.shh, privateKey) +} + +func (s *MessengerPushNotificationSuite) newPushNotificationServer(shh types.Waku, privateKey *ecdsa.PrivateKey) *Messenger { + + tmpFile, err := ioutil.TempFile("", "") + s.Require().NoError(err) + + serverConfig := &pushnotificationserver.Config{ + Logger: s.logger, + Identity: privateKey, + } + + options := []Option{ + WithCustomLogger(s.logger), + WithMessagesPersistenceEnabled(), + WithDatabaseConfig(tmpFile.Name(), "some-key"), + WithPushNotificationServerConfig(serverConfig), + WithDatasync(), + } + return s.newMessengerWithOptions(shh, privateKey, options) +} + +func (s *MessengerPushNotificationSuite) TestReceivePushNotification() { + + bob1 := s.m + bob2 := s.newMessengerWithKey(s.shh, s.m.identity) + + serverKey, err := crypto.GenerateKey() + s.Require().NoError(err) + server := s.newPushNotificationServer(s.shh, serverKey) + + alice := s.newMessenger(s.shh) + // start alice and enable sending push notifications + s.Require().NoError(alice.Start()) + s.Require().NoError(alice.EnableSendingPushNotifications()) + bobInstallationIDs := []string{bob1.installationID, bob2.installationID} + + // Register bob1 + err = bob1.AddPushNotificationsServer(context.Background(), &server.identity.PublicKey) + s.Require().NoError(err) + + err = bob1.RegisterForPushNotifications(context.Background(), bob1DeviceToken) + + // Pull servers and check we registered + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = bob1.RetrieveAll() + if err != nil { + return err + } + registered, err := bob1.RegisteredForPushNotifications() + if err != nil { + return err + } + if !registered { + return errors.New("not registered") + } + return nil + }) + // Make sure we receive it + s.Require().NoError(err) + bob1Servers, err := bob1.GetPushNotificationServers() + s.Require().NoError(err) + + // Register bob2 + err = bob2.AddPushNotificationsServer(context.Background(), &server.identity.PublicKey) + s.Require().NoError(err) + + err = bob2.RegisterForPushNotifications(context.Background(), bob2DeviceToken) + s.Require().NoError(err) + + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = bob2.RetrieveAll() + if err != nil { + return err + } + + registered, err := bob2.RegisteredForPushNotifications() + if err != nil { + return err + } + if !registered { + return errors.New("not registered") + } + return nil + }) + // Make sure we receive it + s.Require().NoError(err) + bob2Servers, err := bob2.GetPushNotificationServers() + s.Require().NoError(err) + + // Create one to one chat & send message + pkString := hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey)) + chat := CreateOneToOneChat(pkString, &s.m.identity.PublicKey, alice.transport) + s.Require().NoError(alice.SaveChat(&chat)) + inputMessage := buildTestMessage(chat) + response, err := alice.SendChatMessage(context.Background(), inputMessage) + s.Require().NoError(err) + messageIDString := response.Messages[0].ID + messageID, err := hex.DecodeString(messageIDString[2:]) + s.Require().NoError(err) + + var info []*pushnotificationclient.PushNotificationInfo + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + + info, err = alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, bobInstallationIDs) + if err != nil { + return err + } + // Check we have replies for both bob1 and bob2 + if len(info) != 2 { + return errors.New("info not fetched") + } + return nil + + }) + + s.Require().NoError(err) + + // Check we have replies for both bob1 and bob2 + var bob1Info, bob2Info *pushnotificationclient.PushNotificationInfo + + if info[0].AccessToken == bob1Servers[0].AccessToken { + bob1Info = info[0] + bob2Info = info[1] + } else { + bob2Info = info[0] + bob1Info = info[1] + } + + s.Require().NotNil(bob1Info) + s.Require().Equal(bob1.installationID, bob1Info.InstallationID) + s.Require().Equal(bob1Servers[0].AccessToken, bob1Info.AccessToken) + s.Require().Equal(&bob1.identity.PublicKey, bob1Info.PublicKey) + + s.Require().NotNil(bob2Info) + s.Require().Equal(bob2.installationID, bob2Info.InstallationID) + s.Require().Equal(bob2Servers[0].AccessToken, bob2Info.AccessToken) + s.Require().Equal(&bob2.identity.PublicKey, bob2Info.PublicKey) + + retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, bobInstallationIDs) + + s.Require().NoError(err) + s.Require().NotNil(retrievedNotificationInfo) + s.Require().Len(retrievedNotificationInfo, 2) + + var sentNotification *pushnotificationclient.SentNotification + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob1.identity.PublicKey), bob1.installationID, messageID) + if err != nil { + return err + } + if sentNotification == nil { + return errors.New("sent notification not found") + } + if !sentNotification.Success { + return errors.New("sent notification not successul") + } + return nil + }) + s.Require().NoError(err) + s.Require().NoError(bob2.Shutdown()) + s.Require().NoError(alice.Shutdown()) + s.Require().NoError(server.Shutdown()) +} + +func (s *MessengerPushNotificationSuite) TestReceivePushNotificationFromContactOnly() { + + bob := s.m + + serverKey, err := crypto.GenerateKey() + s.Require().NoError(err) + server := s.newPushNotificationServer(s.shh, serverKey) + + alice := s.newMessenger(s.shh) + // start alice and enable push notifications + s.Require().NoError(alice.Start()) + s.Require().NoError(alice.EnableSendingPushNotifications()) + bobInstallationIDs := []string{bob.installationID} + + // Register bob + err = bob.AddPushNotificationsServer(context.Background(), &server.identity.PublicKey) + s.Require().NoError(err) + + // Add alice has a contact + aliceContact := &Contact{ + ID: types.EncodeHex(crypto.FromECDSAPub(&alice.identity.PublicKey)), + Name: "Some Contact", + SystemTags: []string{contactAdded}, + } + + err = bob.SaveContact(aliceContact) + s.Require().NoError(err) + + // Enable from contacts only + err = bob.EnablePushNotificationsFromContactsOnly() + s.Require().NoError(err) + + err = bob.RegisterForPushNotifications(context.Background(), bob1DeviceToken) + s.Require().NoError(err) + + // Pull servers and check we registered + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = bob.RetrieveAll() + if err != nil { + return err + } + registered, err := bob.RegisteredForPushNotifications() + if err != nil { + return err + } + if !registered { + return errors.New("not registered") + } + return nil + }) + // Make sure we receive it + s.Require().NoError(err) + bobServers, err := bob.GetPushNotificationServers() + s.Require().NoError(err) + + // Create one to one chat & send message + pkString := hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey)) + chat := CreateOneToOneChat(pkString, &s.m.identity.PublicKey, alice.transport) + s.Require().NoError(alice.SaveChat(&chat)) + inputMessage := buildTestMessage(chat) + response, err := alice.SendChatMessage(context.Background(), inputMessage) + s.Require().NoError(err) + messageIDString := response.Messages[0].ID + messageID, err := hex.DecodeString(messageIDString[2:]) + s.Require().NoError(err) + + var info []*pushnotificationclient.PushNotificationInfo + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + + info, err = alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs) + if err != nil { + return err + } + // Check we have replies for bob + if len(info) != 1 { + return errors.New("info not fetched") + } + return nil + + }) + s.Require().NoError(err) + + s.Require().NotNil(info) + s.Require().Equal(bob.installationID, info[0].InstallationID) + s.Require().Equal(bobServers[0].AccessToken, info[0].AccessToken) + s.Require().Equal(&bob.identity.PublicKey, info[0].PublicKey) + + retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs) + s.Require().NoError(err) + s.Require().NotNil(retrievedNotificationInfo) + s.Require().Len(retrievedNotificationInfo, 1) + + var sentNotification *pushnotificationclient.SentNotification + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob.identity.PublicKey), bob.installationID, messageID) + if err != nil { + return err + } + if sentNotification == nil { + return errors.New("sent notification not found") + } + if !sentNotification.Success { + return errors.New("sent notification not successul") + } + return nil + }) + + s.Require().NoError(err) + s.Require().NoError(alice.Shutdown()) + s.Require().NoError(server.Shutdown()) +} + +func (s *MessengerPushNotificationSuite) TestReceivePushNotificationRetries() { + + bob := s.m + + serverKey, err := crypto.GenerateKey() + s.Require().NoError(err) + server := s.newPushNotificationServer(s.shh, serverKey) + + alice := s.newMessenger(s.shh) + // another contact to invalidate the token + frank := s.newMessenger(s.shh) + // start alice and enable push notifications + s.Require().NoError(alice.Start()) + s.Require().NoError(alice.EnableSendingPushNotifications()) + bobInstallationIDs := []string{bob.installationID} + + // Register bob + err = bob.AddPushNotificationsServer(context.Background(), &server.identity.PublicKey) + s.Require().NoError(err) + + // Add alice has a contact + aliceContact := &Contact{ + ID: types.EncodeHex(crypto.FromECDSAPub(&alice.identity.PublicKey)), + Name: "Some Contact", + SystemTags: []string{contactAdded}, + } + + err = bob.SaveContact(aliceContact) + s.Require().NoError(err) + + // Add frank has a contact + frankContact := &Contact{ + ID: types.EncodeHex(crypto.FromECDSAPub(&frank.identity.PublicKey)), + Name: "Some Contact", + SystemTags: []string{contactAdded}, + } + + err = bob.SaveContact(frankContact) + s.Require().NoError(err) + + // Enable from contacts only + err = bob.EnablePushNotificationsFromContactsOnly() + s.Require().NoError(err) + + err = bob.RegisterForPushNotifications(context.Background(), bob1DeviceToken) + s.Require().NoError(err) + + // Pull servers and check we registered + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = bob.RetrieveAll() + if err != nil { + return err + } + registered, err := bob.RegisteredForPushNotifications() + if err != nil { + return err + } + if !registered { + return errors.New("not registered") + } + return nil + }) + // Make sure we receive it + s.Require().NoError(err) + bobServers, err := bob.GetPushNotificationServers() + s.Require().NoError(err) + + // Create one to one chat & send message + pkString := hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey)) + chat := CreateOneToOneChat(pkString, &s.m.identity.PublicKey, alice.transport) + s.Require().NoError(alice.SaveChat(&chat)) + inputMessage := buildTestMessage(chat) + _, err = alice.SendChatMessage(context.Background(), inputMessage) + s.Require().NoError(err) + + // We check that alice retrieves the info from the server + var info []*pushnotificationclient.PushNotificationInfo + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + + info, err = alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs) + if err != nil { + return err + } + // Check we have replies for bob + if len(info) != 1 { + return errors.New("info not fetched") + } + return nil + + }) + s.Require().NoError(err) + + s.Require().NotNil(info) + s.Require().Equal(bob.installationID, info[0].InstallationID) + s.Require().Equal(bobServers[0].AccessToken, info[0].AccessToken) + s.Require().Equal(&bob.identity.PublicKey, info[0].PublicKey) + + // The message has been sent, but not received, now we remove a contact so that the token is invalidated + frankContact = &Contact{ + ID: types.EncodeHex(crypto.FromECDSAPub(&frank.identity.PublicKey)), + Name: "Some Contact", + SystemTags: []string{}, + } + err = bob.SaveContact(frankContact) + s.Require().NoError(err) + + // Re-registration should be triggered, pull from server and bob to check we are correctly registered + // Pull servers and check we registered + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = bob.RetrieveAll() + if err != nil { + return err + } + registered, err := bob.RegisteredForPushNotifications() + if err != nil { + return err + } + if !registered { + return errors.New("not registered") + } + return nil + }) + + newBobServers, err := bob.GetPushNotificationServers() + s.Require().NoError(err) + // Make sure access token is not the same + s.Require().NotEqual(newBobServers[0].AccessToken, bobServers[0].AccessToken) + + // Send another message, here the token will not be valid + inputMessage = buildTestMessage(chat) + response, err := alice.SendChatMessage(context.Background(), inputMessage) + s.Require().NoError(err) + messageIDString := response.Messages[0].ID + messageID, err := hex.DecodeString(messageIDString[2:]) + s.Require().NoError(err) + + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + + info, err = alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs) + if err != nil { + return err + } + // Check we have replies for bob + if len(info) != 1 { + return errors.New("info not fetched") + } + if newBobServers[0].AccessToken != info[0].AccessToken { + return errors.New("still using the old access token") + } + return nil + + }) + s.Require().NoError(err) + + s.Require().NotNil(info) + s.Require().Equal(bob.installationID, info[0].InstallationID) + s.Require().Equal(newBobServers[0].AccessToken, info[0].AccessToken) + s.Require().Equal(&bob.identity.PublicKey, info[0].PublicKey) + + retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob.identity.PublicKey, bobInstallationIDs) + s.Require().NoError(err) + s.Require().NotNil(retrievedNotificationInfo) + s.Require().Len(retrievedNotificationInfo, 1) + + var sentNotification *pushnotificationclient.SentNotification + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob.identity.PublicKey), bob.installationID, messageID) + if err != nil { + return err + } + if sentNotification == nil { + return errors.New("sent notification not found") + } + if !sentNotification.Success { + return errors.New("sent notification not successul") + } + return nil + }) + + s.Require().NoError(err) + s.Require().NoError(alice.Shutdown()) + s.Require().NoError(server.Shutdown()) +} + +// Here bob acts as his own server +func (s *MessengerPushNotificationSuite) TestActAsYourOwnPushNotificationServer() { + bob1 := s.m + server := s.newPushNotificationServer(s.shh, s.m.identity) + bob2 := server + alice := s.newMessenger(s.shh) + // start alice and enable sending push notifications + s.Require().NoError(alice.Start()) + s.Require().NoError(alice.EnableSendingPushNotifications()) + bobInstallationIDs := []string{bob1.installationID, bob2.installationID} + + // Register bob1 + err := bob1.AddPushNotificationsServer(context.Background(), &server.identity.PublicKey) + s.Require().NoError(err) + + err = bob1.RegisterForPushNotifications(context.Background(), bob1DeviceToken) + + // Pull servers and check we registered + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = bob1.RetrieveAll() + if err != nil { + return err + } + registered, err := bob1.RegisteredForPushNotifications() + if err != nil { + return err + } + if !registered { + return errors.New("not registered") + } + return nil + }) + // Make sure we receive it + s.Require().NoError(err) + bob1Servers, err := bob1.GetPushNotificationServers() + s.Require().NoError(err) + + // Register bob2 + err = bob2.AddPushNotificationsServer(context.Background(), &server.identity.PublicKey) + s.Require().NoError(err) + + err = bob2.RegisterForPushNotifications(context.Background(), bob2DeviceToken) + s.Require().NoError(err) + + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = bob2.RetrieveAll() + if err != nil { + return err + } + + registered, err := bob2.RegisteredForPushNotifications() + if err != nil { + return err + } + if !registered { + return errors.New("not registered") + } + return nil + }) + // Make sure we receive it + s.Require().NoError(err) + bob2Servers, err := bob2.GetPushNotificationServers() + s.Require().NoError(err) + + // Create one to one chat & send message + pkString := hex.EncodeToString(crypto.FromECDSAPub(&s.m.identity.PublicKey)) + chat := CreateOneToOneChat(pkString, &s.m.identity.PublicKey, alice.transport) + s.Require().NoError(alice.SaveChat(&chat)) + inputMessage := buildTestMessage(chat) + response, err := alice.SendChatMessage(context.Background(), inputMessage) + s.Require().NoError(err) + messageIDString := response.Messages[0].ID + messageID, err := hex.DecodeString(messageIDString[2:]) + s.Require().NoError(err) + + var info []*pushnotificationclient.PushNotificationInfo + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + + info, err = alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, bobInstallationIDs) + if err != nil { + return err + } + // Check we have replies for both bob1 and bob2 + if len(info) != 2 { + return errors.New("info not fetched") + } + return nil + + }) + + s.Require().NoError(err) + + // Check we have replies for both bob1 and bob2 + var bob1Info, bob2Info *pushnotificationclient.PushNotificationInfo + + if info[0].AccessToken == bob1Servers[0].AccessToken { + bob1Info = info[0] + bob2Info = info[1] + } else { + bob2Info = info[0] + bob1Info = info[1] + } + + s.Require().NotNil(bob1Info) + s.Require().Equal(bob1.installationID, bob1Info.InstallationID) + s.Require().Equal(bob1Servers[0].AccessToken, bob1Info.AccessToken) + s.Require().Equal(&bob1.identity.PublicKey, bob1Info.PublicKey) + + s.Require().NotNil(bob2Info) + s.Require().Equal(bob2.installationID, bob2Info.InstallationID) + s.Require().Equal(bob2Servers[0].AccessToken, bob2Info.AccessToken) + s.Require().Equal(&bob2.identity.PublicKey, bob2Info.PublicKey) + + retrievedNotificationInfo, err := alice.pushNotificationClient.GetPushNotificationInfo(&bob1.identity.PublicKey, bobInstallationIDs) + + s.Require().NoError(err) + s.Require().NotNil(retrievedNotificationInfo) + s.Require().Len(retrievedNotificationInfo, 2) + + var sentNotification *pushnotificationclient.SentNotification + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + sentNotification, err = alice.pushNotificationClient.GetSentNotification(common.HashPublicKey(&bob1.identity.PublicKey), bob1.installationID, messageID) + if err != nil { + return err + } + if sentNotification == nil { + return errors.New("sent notification not found") + } + if !sentNotification.Success { + return errors.New("sent notification not successul") + } + return nil + }) + s.Require().NoError(err) + s.Require().NoError(bob2.Shutdown()) + s.Require().NoError(alice.Shutdown()) +} diff --git a/protocol/pushnotificationclient/client.go b/protocol/pushnotificationclient/client.go new file mode 100644 index 000000000..313d5f7ec --- /dev/null +++ b/protocol/pushnotificationclient/client.go @@ -0,0 +1,1287 @@ +package pushnotificationclient + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/rand" + "encoding/hex" + "encoding/json" + "errors" + "io" + "math" + "sort" + "time" + + "github.com/golang/protobuf/proto" + "github.com/google/uuid" + "go.uber.org/zap" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/crypto/ecies" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/protobuf" +) + +// How does sending notifications work? +// 1) Every time a message is scheduled for sending, it will be received on a channel. +// we keep track on whether we should send a push notification for this message. +// 2) Every time a message is dispatched, we check whether we should send a notification. +// If so, we query the user info if necessary, check which installations we should be targeting +// and notify the server if we have information about the user (i.e a token). +// The logic is complicated by the fact that sometimes messages are batched together (datasync) +// and the fact that sometimes we send messages to all devices (dh messages). +// 3) The server will notify us if the wrong token is used, in which case a loop will be started that +// will re-query and re-send the notification, up to a maximum. + +// How does registering works? +// We register with the server asynchronously, through a loop, that will try to make sure that +// we have registered with all the servers added, until eventually it gives up. + +// A lot of the logic is complicated by the fact that waku/whisper is not req/response, so we just fire a message +// hoping to get a reply at some later stages. + +const encryptedPayloadKeyLength = 16 +const accessTokenKeyLength = 16 +const staleQueryTimeInSeconds = 86400 + +// maxRegistrationRetries is the maximum number of attempts we do before giving up registering with a server +const maxRegistrationRetries int64 = 12 + +// maxPushNotificationRetries is the maximum number of attempts before we give up sending a push notification +const maxPushNotificationRetries int64 = 4 + +// pushNotificationBackoffTime is the step of the exponential backoff +const pushNotificationBackoffTime int64 = 2 + +// RegistrationBackoffTime is the step of the exponential backoff +const RegistrationBackoffTime int64 = 15 + +type PushNotificationServer struct { + PublicKey *ecdsa.PublicKey `json:"-"` + Registered bool `json:"registered,omitempty"` + RegisteredAt int64 `json:"registeredAt,omitempty"` + LastRetriedAt int64 `json:"lastRetriedAt,omitempty"` + RetryCount int64 `json:"retryCount,omitempty"` + AccessToken string `json:"accessToken,omitempty"` +} + +func (s *PushNotificationServer) MarshalJSON() ([]byte, error) { + type ServerAlias PushNotificationServer + item := struct { + *ServerAlias + PublicKeyString string `json:"publicKey"` + }{ + ServerAlias: (*ServerAlias)(s), + PublicKeyString: types.EncodeHex(crypto.FromECDSAPub(s.PublicKey)), + } + + return json.Marshal(item) +} + +type PushNotificationInfo struct { + AccessToken string + InstallationID string + PublicKey *ecdsa.PublicKey + ServerPublicKey *ecdsa.PublicKey + RetrievedAt int64 + Version uint64 +} + +type SentNotification struct { + PublicKey *ecdsa.PublicKey + InstallationID string + LastTriedAt int64 + RetryCount int64 + MessageID []byte + Success bool + Error protobuf.PushNotificationReport_ErrorType +} + +func (s *SentNotification) HashedPublicKey() []byte { + return common.HashPublicKey(s.PublicKey) +} + +type Config struct { + // Identity is our identity key + Identity *ecdsa.PrivateKey + // SendEnabled indicates whether we should be sending push notifications + SendEnabled bool + // RemoteNotificationsEnabled is whether we should register with a remote server for push notifications + RemoteNotificationsEnabled bool + + // AllowyFromContactsOnly indicates whether we should be receiving push notifications + // only from contacts + AllowFromContactsOnly bool + + // InstallationID is the installation-id for this device + InstallationID string + + Logger *zap.Logger + + // TokenType is the type of token + TokenType protobuf.PushNotificationRegistration_TokenType +} + +type Client struct { + persistence *Persistence + + config *Config + + // lastPushNotificationRegistration is the latest known push notification version + lastPushNotificationRegistration *protobuf.PushNotificationRegistration + + // lastContactIDs is the latest contact ids array + lastContactIDs []*ecdsa.PublicKey + + // AccessToken is the access token that is currently being used + AccessToken string + // deviceToken is the device token for this device + deviceToken string + + // randomReader only used for testing so we have deterministic encryption + reader io.Reader + + //messageProcessor is a message processor used to send and being notified of messages + messageProcessor *common.MessageProcessor + + // registrationLoopQuitChan is a channel to indicate to the registration loop that should be terminating + registrationLoopQuitChan chan struct{} + + // resendingLoopQuitChan is a channel to indicate to the send loop that should be terminating + resendingLoopQuitChan chan struct{} + + quit chan struct{} +} + +func New(persistence *Persistence, config *Config, processor *common.MessageProcessor) *Client { + return &Client{ + quit: make(chan struct{}), + config: config, + messageProcessor: processor, + persistence: persistence, + reader: rand.Reader, + } +} + +func (c *Client) Start() error { + if c.messageProcessor == nil { + return errors.New("can't start, missing message processor") + } + + err := c.loadLastPushNotificationRegistration() + if err != nil { + return err + } + + c.subscribeForSentMessages() + c.subscribeForScheduledMessages() + c.startRegistrationLoop() + c.startResendingLoop() + + return nil +} + +func (c *Client) Stop() error { + close(c.quit) + c.stopRegistrationLoop() + c.stopResendingLoop() + return nil +} + +// Unregister unregisters from all the servers +func (c *Client) Unregister() error { + // stop registration loop + c.stopRegistrationLoop() + + registration := c.buildPushNotificationUnregisterMessage() + err := c.saveLastPushNotificationRegistration(registration, nil) + if err != nil { + return err + } + + // reset servers + err = c.resetServers() + if err != nil { + return err + } + + // and asynchronously register + c.startRegistrationLoop() + return nil +} + +// Registered returns true if we registered with all the servers +func (c *Client) Registered() (bool, error) { + servers, err := c.persistence.GetServers() + if err != nil { + return false, err + } + + for _, s := range servers { + if !s.Registered { + return false, nil + } + } + + return true, nil +} + +func (c *Client) GetSentNotification(hashedPublicKey []byte, installationID string, messageID []byte) (*SentNotification, error) { + return c.persistence.GetSentNotification(hashedPublicKey, installationID, messageID) +} + +func (c *Client) GetServers() ([]*PushNotificationServer, error) { + return c.persistence.GetServers() +} + +func (c *Client) Reregister(contactIDs []*ecdsa.PublicKey, mutedChatIDs []string) error { + c.config.Logger.Debug("re-registering") + if len(c.deviceToken) == 0 { + c.config.Logger.Info("no device token, not registering") + return nil + } + + return c.Register(c.deviceToken, contactIDs, mutedChatIDs) +} + +// Register registers with all the servers +func (c *Client) Register(deviceToken string, contactIDs []*ecdsa.PublicKey, mutedChatIDs []string) error { + // stop registration loop + c.stopRegistrationLoop() + + // reset servers + err := c.resetServers() + if err != nil { + return err + } + + c.deviceToken = deviceToken + + registration, err := c.buildPushNotificationRegistrationMessage(contactIDs, mutedChatIDs) + if err != nil { + return err + } + + err = c.saveLastPushNotificationRegistration(registration, contactIDs) + if err != nil { + return err + } + + c.startRegistrationLoop() + + return nil +} + +// HandlePushNotificationRegistrationResponse should check whether the response was successful or not, retry if necessary otherwise store the result in the database +func (c *Client) HandlePushNotificationRegistrationResponse(publicKey *ecdsa.PublicKey, response protobuf.PushNotificationRegistrationResponse) error { + c.config.Logger.Debug("received push notification registration response", zap.Any("response", response)) + + // Not successful ignore for now + if !response.Success { + return errors.New("response was not successful") + } + + servers, err := c.persistence.GetServersByPublicKey([]*ecdsa.PublicKey{publicKey}) + if err != nil { + return err + } + + // we haven't registered with this server + if len(servers) != 1 { + return errors.New("not registered with this server, ignoring") + } + + server := servers[0] + server.Registered = true + server.RegisteredAt = time.Now().Unix() + + return c.persistence.UpsertServer(server) +} + +// HandlePushNotificationQueryResponse should update the data in the database for a given user +func (c *Client) HandlePushNotificationQueryResponse(serverPublicKey *ecdsa.PublicKey, response protobuf.PushNotificationQueryResponse) error { + c.config.Logger.Debug("received push notification query response", zap.Any("response", response)) + if len(response.Info) == 0 { + return errors.New("empty response from the server") + } + + // get the public key associated with this query + publicKey, err := c.persistence.GetQueryPublicKey(response.MessageId) + if err != nil { + return err + } + if publicKey == nil { + c.config.Logger.Debug("query not found") + return nil + } + + var pushNotificationInfo []*PushNotificationInfo + for _, info := range response.Info { + // make sure the public key matches + if !bytes.Equal(info.PublicKey, common.HashPublicKey(publicKey)) { + c.config.Logger.Warn("reply for different key, ignoring") + continue + } + + accessToken := info.AccessToken + + // the user wants notification from contacts only, try to decrypt the access token to see if we are in their contacts + if len(accessToken) == 0 && len(info.AllowedKeyList) != 0 { + accessToken = c.handleAllowedKeyList(publicKey, info.AllowedKeyList) + + } + + // no luck + if len(accessToken) == 0 { + c.config.Logger.Debug("not in the allowed key list") + continue + } + + // We check the user has allowed this server to store this particular + // access token, otherwise anyone could reply with a fake token + // and receive notifications for a user + if err := c.handleGrant(publicKey, serverPublicKey, info.Grant, accessToken); err != nil { + c.config.Logger.Warn("grant verification failed, ignoring", zap.Error(err)) + continue + } + + pushNotificationInfo = append(pushNotificationInfo, &PushNotificationInfo{ + PublicKey: publicKey, + ServerPublicKey: serverPublicKey, + AccessToken: accessToken, + InstallationID: info.InstallationId, + Version: info.Version, + RetrievedAt: time.Now().Unix(), + }) + + } + + err = c.persistence.SavePushNotificationInfo(pushNotificationInfo) + if err != nil { + c.config.Logger.Error("failed to save push notifications", zap.Error(err)) + return err + } + + return nil +} + +// HandlePushNotificationResponse should set the request as processed +func (c *Client) HandlePushNotificationResponse(serverKey *ecdsa.PublicKey, response protobuf.PushNotificationResponse) error { + messageID := response.MessageId + c.config.Logger.Debug("received response for", zap.Binary("message-id", messageID)) + for _, report := range response.Reports { + c.config.Logger.Debug("received response", zap.Any("report", report)) + err := c.persistence.UpdateNotificationResponse(messageID, report) + if err != nil { + return err + } + } + + // Restart resending loop, in case we need to resend some notifications + c.stopResendingLoop() + c.startResendingLoop() + return nil +} + +func (c *Client) RemovePushNotificationServer(publicKey *ecdsa.PublicKey) error { + c.config.Logger.Debug("removing push notification server", zap.Any("public-key", publicKey)) + //TODO: this needs implementing. It requires unregistering from the server and + // likely invalidate the device token of the user + return errors.New("not implemented") +} + +func (c *Client) AddPushNotificationsServer(publicKey *ecdsa.PublicKey) error { + c.config.Logger.Debug("adding push notifications server", zap.Any("public-key", publicKey)) + currentServers, err := c.persistence.GetServers() + if err != nil { + return err + } + + for _, server := range currentServers { + if common.IsPubKeyEqual(server.PublicKey, publicKey) { + return errors.New("push notification server already added") + } + } + + err = c.persistence.UpsertServer(&PushNotificationServer{ + PublicKey: publicKey, + }) + if err != nil { + return err + } + + if c.config.RemoteNotificationsEnabled { + c.startRegistrationLoop() + } + return nil +} + +func (c *Client) GetPushNotificationInfo(publicKey *ecdsa.PublicKey, installationIDs []string) ([]*PushNotificationInfo, error) { + if len(installationIDs) == 0 { + return c.persistence.GetPushNotificationInfoByPublicKey(publicKey) + } + return c.persistence.GetPushNotificationInfo(publicKey, installationIDs) +} + +func (c *Client) EnableSending() { + c.config.SendEnabled = true +} + +func (c *Client) DisableSending() { + c.config.SendEnabled = false +} + +func (c *Client) EnablePushNotificationsFromContactsOnly(contactIDs []*ecdsa.PublicKey, mutedChatIDs []string) error { + c.config.AllowFromContactsOnly = true + if c.lastPushNotificationRegistration != nil { + return c.Register(c.deviceToken, contactIDs, mutedChatIDs) + } + return nil +} + +func (c *Client) DisablePushNotificationsFromContactsOnly(contactIDs []*ecdsa.PublicKey, mutedChatIDs []string) error { + c.config.AllowFromContactsOnly = false + if c.lastPushNotificationRegistration != nil { + return c.Register(c.deviceToken, contactIDs, mutedChatIDs) + } + return nil +} + +func encryptAccessToken(plaintext []byte, key []byte, reader io.Reader) ([]byte, error) { + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(reader, nonce); err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, plaintext, nil), nil +} + +func (c *Client) encryptRegistration(publicKey *ecdsa.PublicKey, payload []byte) ([]byte, error) { + sharedKey, err := c.generateSharedKey(publicKey) + if err != nil { + return nil, err + } + + return common.Encrypt(payload, sharedKey, c.reader) +} + +func (c *Client) generateSharedKey(publicKey *ecdsa.PublicKey) ([]byte, error) { + return ecies.ImportECDSA(c.config.Identity).GenerateShared( + ecies.ImportECDSAPublic(publicKey), + encryptedPayloadKeyLength, + encryptedPayloadKeyLength, + ) +} + +// subscribeForSentMessages subscribes for newly sent messages so we can check if we need to send a push notification +func (c *Client) subscribeForSentMessages() { + go func() { + c.config.Logger.Debug("subscribing for sent messages") + subscription := c.messageProcessor.SubscribeToSentMessages() + for { + select { + case m, more := <-subscription: + if !more { + c.config.Logger.Debug("no more sent messages, quitting") + return + } + c.config.Logger.Debug("handling message sent") + if err := c.handleMessageSent(m); err != nil { + c.config.Logger.Error("failed to handle message", zap.Error(err)) + } + case <-c.quit: + return + } + } + }() +} + +// subscribeForScheduledMessages subscribes for messages scheduler for dispatch +func (c *Client) subscribeForScheduledMessages() { + go func() { + c.config.Logger.Debug("subscribing for scheduled messages") + subscription := c.messageProcessor.SubscribeToScheduledMessages() + for { + select { + case m, more := <-subscription: + if !more { + c.config.Logger.Debug("no more scheduled messages, quitting") + return + } + c.config.Logger.Debug("handling message scheduled") + if err := c.handleMessageScheduled(m); err != nil { + c.config.Logger.Error("failed to handle message", zap.Error(err)) + } + case <-c.quit: + return + } + } + }() +} + +// loadLastPushNotificationRegistration loads from the database the last registration +func (c *Client) loadLastPushNotificationRegistration() error { + lastRegistration, lastContactIDs, err := c.persistence.GetLastPushNotificationRegistration() + if err != nil { + return err + } + if lastRegistration == nil { + lastRegistration = &protobuf.PushNotificationRegistration{} + } + c.lastContactIDs = lastContactIDs + c.lastPushNotificationRegistration = lastRegistration + c.deviceToken = lastRegistration.DeviceToken + return nil + +} + +func (c *Client) stopRegistrationLoop() { + // stop old registration loop + if c.registrationLoopQuitChan != nil { + close(c.registrationLoopQuitChan) + c.registrationLoopQuitChan = nil + } +} + +func (c *Client) stopResendingLoop() { + // stop old registration loop + if c.resendingLoopQuitChan != nil { + close(c.resendingLoopQuitChan) + c.resendingLoopQuitChan = nil + } +} + +func (c *Client) startRegistrationLoop() { + c.stopRegistrationLoop() + c.registrationLoopQuitChan = make(chan struct{}) + go func() { + err := c.registrationLoop() + if err != nil { + c.config.Logger.Error("registration loop exited with an error", zap.Error(err)) + } + }() +} + +func (c *Client) startResendingLoop() { + c.stopResendingLoop() + c.resendingLoopQuitChan = make(chan struct{}) + go func() { + err := c.resendingLoop() + if err != nil { + c.config.Logger.Error("resending loop exited with an error", zap.Error(err)) + } + }() +} + +// queryNotificationInfo will block and query for the client token, if force is set it +// will ignore the cool off period +func (c *Client) queryNotificationInfo(publicKey *ecdsa.PublicKey, force bool) error { + c.config.Logger.Debug("retrieving queried at") + + // Check if we queried recently + queriedAt, err := c.persistence.GetQueriedAt(publicKey) + if err != nil { + c.config.Logger.Error("failed to retrieve queried at", zap.Error(err)) + return err + } + c.config.Logger.Debug("checking if querying necessary") + + // Naively query again if too much time has passed. + // Here it might not be necessary + if force || time.Now().Unix()-queriedAt > staleQueryTimeInSeconds { + c.config.Logger.Debug("querying info") + err := c.queryPushNotificationInfo(publicKey) + if err != nil { + c.config.Logger.Error("could not query pn info", zap.Error(err)) + return err + } + // This is just horrible, but for now will do, + // the issue is that we don't really know how long it will + // take to reply, as there might be multiple servers + // replying to us. + // The only time we are 100% certain that we can proceed is + // when we have non-stale info for each device, but + // most devices are not going to be registered, so we'd still + // have to wait the maximum amount of time allowed. + // A better way to handle this is to set a maximum timer of say + // 3 seconds, but act at a tick every 200ms. + // That way we still are able to batch multiple push notifications + // but we don't have to wait every time 3 seconds, which is wasteful + // This probably will have to be addressed before released + time.Sleep(3 * time.Second) + } + return nil +} + +// handleMessageSent is called every time a message is sent. It will check if +// we need to notify on the message, and if so it will try to dispatch a push notification +// messages might be batched, if coming from datasync for example. +func (c *Client) handleMessageSent(sentMessage *common.SentMessage) error { + c.config.Logger.Debug("sent messages", zap.Any("messageIDs", sentMessage.MessageIDs)) + + // Ignore if we are not sending notifications + if !c.config.SendEnabled { + c.config.Logger.Debug("send not enabled, ignoring") + return nil + } + + publicKey := sentMessage.PublicKey + + // Collect the messageIDs we want to notify on + var trackedMessageIDs [][]byte + + for _, messageID := range sentMessage.MessageIDs { + tracked, err := c.persistence.TrackedMessage(messageID) + if err != nil { + return err + } + if tracked { + trackedMessageIDs = append(trackedMessageIDs, messageID) + } + } + + // Nothing to do + if len(trackedMessageIDs) == 0 { + c.config.Logger.Debug("nothing to do for", zap.Any("messageIDs", sentMessage.MessageIDs)) + return nil + } + + // sendToAllDevices indicates whether the message has been sent using public key encryption only + // i.e not through the double ratchet. In that case, any device will have received it. + sendToAllDevices := len(sentMessage.Spec.Installations) == 0 + + var installationIDs []string + + anyActionableMessage := sendToAllDevices + + // Check if we should be notifiying those installations + for _, messageID := range trackedMessageIDs { + for _, installation := range sentMessage.Spec.Installations { + installationID := installation.ID + shouldNotify, err := c.shouldNotifyOn(publicKey, installationID, messageID) + if err != nil { + return err + } + if shouldNotify { + anyActionableMessage = true + installationIDs = append(installationIDs, installation.ID) + } + } + } + + // Is there anything we should be notifying on? + if !anyActionableMessage { + c.config.Logger.Debug("no actionable installation IDs") + return nil + } + + c.config.Logger.Debug("actionable messages", zap.Any("message-ids", trackedMessageIDs), zap.Any("installation-ids", installationIDs)) + + // we send the notifications and return the info of the devices notified + infos, err := c.sendNotification(publicKey, installationIDs, trackedMessageIDs[0]) + if err != nil { + return err + } + + // mark message as sent so we don't notify again + for _, i := range infos { + for _, messageID := range trackedMessageIDs { + + c.config.Logger.Debug("marking as sent ", zap.Binary("mid", messageID), zap.String("id", i.InstallationID)) + if err := c.notifiedOn(publicKey, i.InstallationID, messageID); err != nil { + return err + } + + } + } + + return nil +} + +// handleMessageScheduled keeps track of the message to make sure we notify on it +func (c *Client) handleMessageScheduled(message *common.RawMessage) error { + if !message.SendPushNotification { + return nil + } + messageID, err := types.DecodeHex(message.ID) + if err != nil { + return err + } + return c.persistence.TrackPushNotification(message.LocalChatID, messageID) +} + +// shouldNotifyOn check whether we should notify a particular public-key/installation-id/message-id combination +func (c *Client) shouldNotifyOn(publicKey *ecdsa.PublicKey, installationID string, messageID []byte) (bool, error) { + if len(installationID) == 0 { + return c.persistence.ShouldSendNotificationToAllInstallationIDs(publicKey, messageID) + } + return c.persistence.ShouldSendNotificationFor(publicKey, installationID, messageID) +} + +// notifiedOn marks a combination of publickey/installationid/messageID as notified +func (c *Client) notifiedOn(publicKey *ecdsa.PublicKey, installationID string, messageID []byte) error { + return c.persistence.UpsertSentNotification(&SentNotification{ + PublicKey: publicKey, + LastTriedAt: time.Now().Unix(), + InstallationID: installationID, + MessageID: messageID, + }) +} + +func (c *Client) mutedChatIDsHashes(chatIDs []string) [][]byte { + var mutedChatListHashes [][]byte + + for _, chatID := range chatIDs { + mutedChatListHashes = append(mutedChatListHashes, common.Shake256([]byte(chatID))) + } + + return mutedChatListHashes +} + +func (c *Client) encryptToken(publicKey *ecdsa.PublicKey, token []byte) ([]byte, error) { + sharedKey, err := ecies.ImportECDSA(c.config.Identity).GenerateShared( + ecies.ImportECDSAPublic(publicKey), + accessTokenKeyLength, + accessTokenKeyLength, + ) + if err != nil { + return nil, err + } + encryptedToken, err := encryptAccessToken(token, sharedKey, c.reader) + if err != nil { + return nil, err + } + return encryptedToken, nil +} + +func (c *Client) decryptToken(publicKey *ecdsa.PublicKey, token []byte) ([]byte, error) { + sharedKey, err := ecies.ImportECDSA(c.config.Identity).GenerateShared( + ecies.ImportECDSAPublic(publicKey), + accessTokenKeyLength, + accessTokenKeyLength, + ) + if err != nil { + return nil, err + } + decryptedToken, err := common.Decrypt(token, sharedKey) + if err != nil { + return nil, err + } + return decryptedToken, nil +} + +// allowedKeyList builds up a list of encrypted tokens, used for registering with the server +func (c *Client) allowedKeyList(token []byte, contactIDs []*ecdsa.PublicKey) ([][]byte, error) { + // If we allow everyone, don't set the list + if !c.config.AllowFromContactsOnly { + return nil, nil + } + var encryptedTokens [][]byte + for _, publicKey := range contactIDs { + encryptedToken, err := c.encryptToken(publicKey, token) + if err != nil { + return nil, err + } + + encryptedTokens = append(encryptedTokens, encryptedToken) + + } + return encryptedTokens, nil +} + +// getToken checks if we need to refresh the token +// and return a new one in that case. A token is refreshed only if it's not set +// or if a contact has been removed +func (c *Client) getToken(contactIDs []*ecdsa.PublicKey) string { + if c.lastPushNotificationRegistration == nil || len(c.lastPushNotificationRegistration.AccessToken) == 0 || c.shouldRefreshToken(c.lastContactIDs, contactIDs) { + c.config.Logger.Info("refreshing access token") + return uuid.New().String() + } + return c.lastPushNotificationRegistration.AccessToken +} + +func (c *Client) getVersion() uint64 { + if c.lastPushNotificationRegistration == nil { + return 1 + } + return c.lastPushNotificationRegistration.Version + 1 +} + +func (c *Client) buildPushNotificationRegistrationMessage(contactIDs []*ecdsa.PublicKey, mutedChatIDs []string) (*protobuf.PushNotificationRegistration, error) { + token := c.getToken(contactIDs) + allowedKeyList, err := c.allowedKeyList([]byte(token), contactIDs) + if err != nil { + return nil, err + } + + options := &protobuf.PushNotificationRegistration{ + AccessToken: token, + TokenType: c.config.TokenType, + Version: c.getVersion(), + InstallationId: c.config.InstallationID, + DeviceToken: c.deviceToken, + AllowFromContactsOnly: c.config.AllowFromContactsOnly, + Enabled: c.config.RemoteNotificationsEnabled, + BlockedChatList: c.mutedChatIDsHashes(mutedChatIDs), + AllowedKeyList: allowedKeyList, + } + return options, nil +} + +func (c *Client) buildPushNotificationUnregisterMessage() *protobuf.PushNotificationRegistration { + options := &protobuf.PushNotificationRegistration{ + Version: c.getVersion(), + InstallationId: c.config.InstallationID, + Unregister: true, + } + return options +} + +// shouldRefreshToken tells us whether we should pull a new token, that's only necessary when a contact is removed +func (c *Client) shouldRefreshToken(oldContactIDs, newContactIDs []*ecdsa.PublicKey) bool { + newContactIDsMap := make(map[string]bool) + for _, pk := range newContactIDs { + newContactIDsMap[types.EncodeHex(crypto.FromECDSAPub(pk))] = true + } + + for _, pk := range oldContactIDs { + if ok := newContactIDsMap[types.EncodeHex(crypto.FromECDSAPub(pk))]; !ok { + return true + } + + } + return false +} + +func nextServerRetry(server *PushNotificationServer) int64 { + return server.LastRetriedAt + RegistrationBackoffTime*server.RetryCount*int64(math.Exp2(float64(server.RetryCount))) +} + +func nextPushNotificationRetry(pn *SentNotification) int64 { + return pn.LastTriedAt + pushNotificationBackoffTime*pn.RetryCount*int64(math.Exp2(float64(pn.RetryCount))) +} + +// We calculate if it's too early to retry, by exponentially backing off +func shouldRetryRegisteringWithServer(server *PushNotificationServer) bool { + return time.Now().Unix() >= nextServerRetry(server) +} + +// We calculate if it's too early to retry, by exponentially backing off +func shouldRetryPushNotification(pn *SentNotification) bool { + if pn.RetryCount > maxPushNotificationRetries { + return false + } + return time.Now().Unix() >= nextPushNotificationRetry(pn) +} + +func (c *Client) resetServers() error { + servers, err := c.persistence.GetServers() + if err != nil { + return err + } + for _, server := range servers { + + // Reset server registration data + server.Registered = false + server.RegisteredAt = 0 + server.RetryCount = 0 + server.LastRetriedAt = time.Now().Unix() + server.AccessToken = "" + + if err := c.persistence.UpsertServer(server); err != nil { + return err + } + } + + return nil +} + +// registerWithServer will register with a push notification server. This will use +// the user identity key for dispatching, as the content is in any case signed, so identity needs to be revealed. +func (c *Client) registerWithServer(registration *protobuf.PushNotificationRegistration, server *PushNotificationServer) error { + // reset server registration data + server.Registered = false + server.RegisteredAt = 0 + server.RetryCount++ + server.LastRetriedAt = time.Now().Unix() + server.AccessToken = registration.AccessToken + + // save + if err := c.persistence.UpsertServer(server); err != nil { + return err + } + + // build grant for this specific server + grant, err := c.buildGrantSignature(server.PublicKey, registration.AccessToken) + if err != nil { + c.config.Logger.Error("failed to build grant", zap.Error(err)) + return err + } + + registration.Grant = grant + + // marshal message + marshaledRegistration, err := proto.Marshal(registration) + if err != nil { + return err + } + + // encrypt and dispatch message + encryptedRegistration, err := c.encryptRegistration(server.PublicKey, marshaledRegistration) + if err != nil { + return err + } + rawMessage := &common.RawMessage{ + Payload: encryptedRegistration, + MessageType: protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION, + } + + _, err = c.messageProcessor.SendPrivate(context.Background(), server.PublicKey, rawMessage) + + if err != nil { + return err + } + return nil +} + +// sendNotification sends an actual notification to the push notification server. +// the notification is sent using an ephemeral key to shield the real identity of the sender +func (c *Client) sendNotification(publicKey *ecdsa.PublicKey, installationIDs []string, messageID []byte) ([]*PushNotificationInfo, error) { + // get latest push notification infos + err := c.queryNotificationInfo(publicKey, false) + if err != nil { + return nil, err + } + c.config.Logger.Debug("queried info") + + // retrieve info from the database + info, err := c.GetPushNotificationInfo(publicKey, installationIDs) + if err != nil { + c.config.Logger.Error("could not get pn info", zap.Error(err)) + return nil, err + } + + // naively dispatch to the first server for now + // push notifications are only retried for now if a WRONG_TOKEN response is returned. + // we should also retry if no response at all is received after a timeout. + // also we send a single notification for multiple message ids, need to check with UI what's the desired behavior + + // sort by server so we tend to hit the same one + sort.Slice(info, func(i, j int) bool { + return info[i].ServerPublicKey.X.Cmp(info[j].ServerPublicKey.X) <= 0 + }) + + installationIDsMap := make(map[string]bool) + + // one info per installation id, grouped by server + actionableInfos := make(map[string][]*PushNotificationInfo) + for _, i := range info { + + if !installationIDsMap[i.InstallationID] { + serverKey := hex.EncodeToString(crypto.CompressPubkey(i.ServerPublicKey)) + actionableInfos[serverKey] = append(actionableInfos[serverKey], i) + installationIDsMap[i.InstallationID] = true + } + + } + + c.config.Logger.Debug("actionable info", zap.Int("count", len(actionableInfos))) + + // add ephemeral key and listen to it + ephemeralKey, err := crypto.GenerateKey() + if err != nil { + return nil, err + } + _, err = c.messageProcessor.AddEphemeralKey(ephemeralKey) + if err != nil { + return nil, err + } + + var actionedInfo []*PushNotificationInfo + for _, infos := range actionableInfos { + var pushNotifications []*protobuf.PushNotification + for _, i := range infos { + // TODO: Add ChatID, message, public_key + pushNotifications = append(pushNotifications, &protobuf.PushNotification{ + AccessToken: i.AccessToken, + PublicKey: common.HashPublicKey(publicKey), + InstallationId: i.InstallationID, + }) + + } + request := &protobuf.PushNotificationRequest{ + MessageId: messageID, + Requests: pushNotifications, + } + serverPublicKey := infos[0].ServerPublicKey + + payload, err := proto.Marshal(request) + if err != nil { + return nil, err + } + + rawMessage := &common.RawMessage{ + Payload: payload, + Sender: ephemeralKey, + // we skip encryption as we don't want to save any key material + // for an ephemeral key, no need to use pfs as these are throw away keys + SkipEncryption: true, + MessageType: protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_REQUEST, + } + + _, err = c.messageProcessor.SendPrivate(context.Background(), serverPublicKey, rawMessage) + + if err != nil { + return nil, err + } + actionedInfo = append(actionedInfo, infos...) + } + return actionedInfo, nil +} + +func (c *Client) resendNotification(pn *SentNotification) error { + c.config.Logger.Debug("resending notification") + pn.RetryCount++ + pn.LastTriedAt = time.Now().Unix() + err := c.persistence.UpsertSentNotification(pn) + if err != nil { + c.config.Logger.Error("failed to upsert notification", zap.Error(err)) + return err + } + + // re-fetch push notification info + err = c.queryNotificationInfo(pn.PublicKey, true) + if err != nil { + c.config.Logger.Error("failed to query notification info", zap.Error(err)) + return err + } + + if err != nil { + c.config.Logger.Error("could not get pn info", zap.Error(err)) + return err + } + + _, err = c.sendNotification(pn.PublicKey, []string{pn.InstallationID}, pn.MessageID) + return err +} + +// resendingLoop is a loop that is running when push notifications need to be resent, it only runs when needed, it will quit if no work is necessary. +func (c *Client) resendingLoop() error { + for { + c.config.Logger.Debug("running resending loop") + var lowestNextRetry int64 + + // fetch retriable notifications + retriableNotifications, err := c.persistence.GetRetriablePushNotifications() + if err != nil { + c.config.Logger.Error("failed retrieving notifications, quitting resending loop", zap.Error(err)) + return err + } + + if len(retriableNotifications) == 0 { + c.config.Logger.Debug("no retriable notifications, quitting") + return nil + } + + for _, pn := range retriableNotifications { + + // check if we should retry the notification + if shouldRetryPushNotification(pn) { + c.config.Logger.Debug("retrying pn") + err := c.resendNotification(pn) + if err != nil { + return err + } + } + // set the lowest next retry if necessary + nextRetry := nextPushNotificationRetry(pn) + if lowestNextRetry == 0 || nextRetry < lowestNextRetry { + lowestNextRetry = nextRetry + } + } + + nextRetry := lowestNextRetry - time.Now().Unix() + // how long should we sleep for? + waitFor := time.Duration(nextRetry) + select { + + case <-time.After(waitFor * time.Second): + case <-c.resendingLoopQuitChan: + return nil + } + } +} + +// registrationLoop is a loop that is running when we need to register with a push notification server, it only runs when needed, it will quit if no work is necessary. +func (c *Client) registrationLoop() error { + for { + c.config.Logger.Debug("running registration loop") + servers, err := c.persistence.GetServers() + if err != nil { + c.config.Logger.Error("failed retrieving servers, quitting registration loop", zap.Error(err)) + return err + } + if len(servers) == 0 { + c.config.Logger.Debug("nothing to do, quitting registration loop") + return nil + } + + var nonRegisteredServers []*PushNotificationServer + for _, server := range servers { + if !server.Registered && server.RetryCount < maxRegistrationRetries { + nonRegisteredServers = append(nonRegisteredServers, server) + } + } + + if len(nonRegisteredServers) == 0 { + c.config.Logger.Debug("registered with all servers, quitting registration loop") + return nil + } + + c.config.Logger.Debug("Trying to register with", zap.Int("servers", len(nonRegisteredServers))) + + var lowestNextRetry int64 + + for _, server := range nonRegisteredServers { + if shouldRetryRegisteringWithServer(server) { + c.config.Logger.Debug("registering with server", zap.Any("server", server)) + err := c.registerWithServer(c.lastPushNotificationRegistration, server) + if err != nil { + return err + } + } + nextRetry := nextServerRetry(server) + if lowestNextRetry == 0 || nextRetry < lowestNextRetry { + lowestNextRetry = nextRetry + } + } + + nextRetry := lowestNextRetry - time.Now().Unix() + waitFor := time.Duration(nextRetry) + c.config.Logger.Debug("Waiting for", zap.Any("wait for", waitFor)) + select { + + case <-time.After(waitFor * time.Second): + case <-c.registrationLoopQuitChan: + return nil + } + } +} + +func (c *Client) saveLastPushNotificationRegistration(registration *protobuf.PushNotificationRegistration, contactIDs []*ecdsa.PublicKey) error { + // stop registration loop + c.stopRegistrationLoop() + + err := c.persistence.SaveLastPushNotificationRegistration(registration, contactIDs) + if err != nil { + return err + } + c.lastPushNotificationRegistration = registration + c.lastContactIDs = contactIDs + + c.startRegistrationLoop() + return nil +} + +// buildGrantSignatureMaterial builds a grant for a specific server. +// We use 3 components: +// 1) The client public key. Not sure this applies to our signature scheme, but best to be conservative. https://crypto.stackexchange.com/questions/15538/given-a-message-and-signature-find-a-public-key-that-makes-the-signature-valid +// 2) The server public key +// 3) The access token +// By verifying this signature, a client can trust the server was instructed to store this access token. + +func (c *Client) buildGrantSignatureMaterial(clientPublicKey *ecdsa.PublicKey, serverPublicKey *ecdsa.PublicKey, accessToken string) []byte { + var signatureMaterial []byte + signatureMaterial = append(signatureMaterial, crypto.CompressPubkey(clientPublicKey)...) + signatureMaterial = append(signatureMaterial, crypto.CompressPubkey(serverPublicKey)...) + signatureMaterial = append(signatureMaterial, []byte(accessToken)...) + return crypto.Keccak256(signatureMaterial) +} + +func (c *Client) buildGrantSignature(serverPublicKey *ecdsa.PublicKey, accessToken string) ([]byte, error) { + signatureMaterial := c.buildGrantSignatureMaterial(&c.config.Identity.PublicKey, serverPublicKey, accessToken) + return crypto.Sign(signatureMaterial, c.config.Identity) +} + +func (c *Client) handleGrant(clientPublicKey *ecdsa.PublicKey, serverPublicKey *ecdsa.PublicKey, grant []byte, accessToken string) error { + signatureMaterial := c.buildGrantSignatureMaterial(clientPublicKey, serverPublicKey, accessToken) + extractedPublicKey, err := crypto.SigToPub(signatureMaterial, grant) + if err != nil { + return err + } + + if !common.IsPubKeyEqual(clientPublicKey, extractedPublicKey) { + return errors.New("invalid grant") + } + return nil +} + +// handleAllowedKeyList will try to decrypt a token from the list, to see if we are allowed to send push notification to a given user +func (c *Client) handleAllowedKeyList(publicKey *ecdsa.PublicKey, allowedKeyList [][]byte) string { + c.config.Logger.Debug("handling allowed key list") + for _, encryptedToken := range allowedKeyList { + token, err := c.decryptToken(publicKey, encryptedToken) + if err != nil { + c.config.Logger.Warn("could not decrypt token", zap.Error(err)) + continue + } + c.config.Logger.Debug("decrypted token") + return string(token) + } + return "" +} + +// queryPushNotificationInfo sends a message to any server who has the given user registered. +// it uses an ephemeral key so the identity of the client querying is not disclosed +func (c *Client) queryPushNotificationInfo(publicKey *ecdsa.PublicKey) error { + hashedPublicKey := common.HashPublicKey(publicKey) + query := &protobuf.PushNotificationQuery{ + PublicKeys: [][]byte{hashedPublicKey}, + } + encodedMessage, err := proto.Marshal(query) + if err != nil { + return err + } + + ephemeralKey, err := crypto.GenerateKey() + if err != nil { + return err + } + + rawMessage := &common.RawMessage{ + Payload: encodedMessage, + Sender: ephemeralKey, + // we don't want to wrap in an encryption layer message + SkipEncryption: true, + MessageType: protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_QUERY, + } + + _, err = c.messageProcessor.AddEphemeralKey(ephemeralKey) + if err != nil { + return err + } + + // this is the topic of message + encodedPublicKey := hex.EncodeToString(hashedPublicKey) + messageID, err := c.messageProcessor.SendPublic(context.Background(), encodedPublicKey, rawMessage) + + if err != nil { + return err + } + + return c.persistence.SavePushNotificationQuery(publicKey, messageID) +} diff --git a/protocol/pushnotificationclient/client_test.go b/protocol/pushnotificationclient/client_test.go new file mode 100644 index 000000000..1c372fdbe --- /dev/null +++ b/protocol/pushnotificationclient/client_test.go @@ -0,0 +1,230 @@ +package pushnotificationclient + +import ( + "bytes" + "crypto/ecdsa" + "io/ioutil" + "math/rand" + "os" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/suite" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/crypto/ecies" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/sqlite" + "github.com/status-im/status-go/protocol/tt" +) + +const testDeviceToken = "test-token" + +type ClientSuite struct { + suite.Suite + tmpFile *os.File + persistence *Persistence + identity *ecdsa.PrivateKey + installationID string + client *Client +} + +func TestClientSuite(t *testing.T) { + s := new(ClientSuite) + s.installationID = "c6ae4fde-bb65-11ea-b3de-0242ac130004" + + suite.Run(t, s) +} + +func (s *ClientSuite) SetupTest() { + tmpFile, err := ioutil.TempFile("", "") + s.Require().NoError(err) + s.tmpFile = tmpFile + + database, err := sqlite.Open(s.tmpFile.Name(), "") + s.Require().NoError(err) + s.persistence = NewPersistence(database) + + identity, err := crypto.GenerateKey() + s.Require().NoError(err) + s.identity = identity + + config := &Config{ + Identity: identity, + Logger: tt.MustCreateTestLogger(), + RemoteNotificationsEnabled: true, + InstallationID: s.installationID, + } + + s.client = New(s.persistence, config, nil) +} + +func (s *ClientSuite) TestBuildPushNotificationRegisterMessage() { + mutedChatList := []string{"a", "b"} + + // build chat lish hashes + var mutedChatListHashes [][]byte + for _, chatID := range mutedChatList { + mutedChatListHashes = append(mutedChatListHashes, common.Shake256([]byte(chatID))) + } + + contactKey, err := crypto.GenerateKey() + s.Require().NoError(err) + contactIDs := []*ecdsa.PublicKey{&contactKey.PublicKey} + + // Set random generator for uuid + var seed int64 = 1 + uuid.SetRand(rand.New(rand.NewSource(seed))) + + // Get token + expectedUUID := uuid.New().String() + + // Reset random generator + uuid.SetRand(rand.New(rand.NewSource(seed))) + + s.client.deviceToken = testDeviceToken + // Set reader + s.client.reader = bytes.NewReader([]byte(expectedUUID)) + + options := &protobuf.PushNotificationRegistration{ + Version: 1, + AccessToken: expectedUUID, + DeviceToken: testDeviceToken, + InstallationId: s.installationID, + Enabled: true, + BlockedChatList: mutedChatListHashes, + } + + actualMessage, err := s.client.buildPushNotificationRegistrationMessage(contactIDs, mutedChatList) + s.Require().NoError(err) + + s.Require().Equal(options, actualMessage) +} + +func (s *ClientSuite) TestBuildPushNotificationRegisterMessageAllowFromContactsOnly() { + mutedChatList := []string{"a", "b"} + + // build chat lish hashes + var mutedChatListHashes [][]byte + for _, chatID := range mutedChatList { + mutedChatListHashes = append(mutedChatListHashes, common.Shake256([]byte(chatID))) + } + + contactKey, err := crypto.GenerateKey() + s.Require().NoError(err) + contactIDs := []*ecdsa.PublicKey{&contactKey.PublicKey} + + // Set random generator for uuid + var seed int64 = 1 + uuid.SetRand(rand.New(rand.NewSource(seed))) + + // Get token + expectedUUID := uuid.New().String() + + // set up reader + reader := bytes.NewReader([]byte(expectedUUID)) + + sharedKey, err := ecies.ImportECDSA(s.identity).GenerateShared( + ecies.ImportECDSAPublic(&contactKey.PublicKey), + accessTokenKeyLength, + accessTokenKeyLength, + ) + s.Require().NoError(err) + // build encrypted token + encryptedToken, err := encryptAccessToken([]byte(expectedUUID), sharedKey, reader) + s.Require().NoError(err) + + // Reset random generator + uuid.SetRand(rand.New(rand.NewSource(seed))) + + s.client.config.AllowFromContactsOnly = true + s.client.deviceToken = testDeviceToken + // Set reader + s.client.reader = bytes.NewReader([]byte(expectedUUID)) + + options := &protobuf.PushNotificationRegistration{ + Version: 1, + AccessToken: expectedUUID, + DeviceToken: testDeviceToken, + InstallationId: s.installationID, + AllowFromContactsOnly: true, + Enabled: true, + BlockedChatList: mutedChatListHashes, + AllowedKeyList: [][]byte{encryptedToken}, + } + + actualMessage, err := s.client.buildPushNotificationRegistrationMessage(contactIDs, mutedChatList) + s.Require().NoError(err) + + s.Require().Equal(options, actualMessage) +} + +func (s *ClientSuite) TestHandleMessageScheduled() { + messageID := []byte("message-id") + chatID := "chat-id" + installationID1 := "1" + installationID2 := "2" + rawMessage := &common.RawMessage{ + ID: types.EncodeHex(messageID), + SendPushNotification: true, + LocalChatID: chatID, + } + + s.Require().NoError(s.client.handleMessageScheduled(rawMessage)) + + key1, err := crypto.GenerateKey() + s.Require().NoError(err) + + // First time, should notify + response, err := s.client.shouldNotifyOn(&key1.PublicKey, installationID1, messageID) + s.Require().NoError(err) + s.Require().True(response) + + // Save notification + s.Require().NoError(s.client.notifiedOn(&key1.PublicKey, installationID1, messageID)) + + // Second time, should not notify + response, err = s.client.shouldNotifyOn(&key1.PublicKey, installationID1, messageID) + s.Require().NoError(err) + s.Require().False(response) + + // Different installationID + response, err = s.client.shouldNotifyOn(&key1.PublicKey, installationID2, messageID) + s.Require().NoError(err) + s.Require().True(response) + + key2, err := crypto.GenerateKey() + s.Require().NoError(err) + // different key, should notify + response, err = s.client.shouldNotifyOn(&key2.PublicKey, installationID1, messageID) + s.Require().NoError(err) + s.Require().True(response) + + // non tracked message id + response, err = s.client.shouldNotifyOn(&key1.PublicKey, installationID1, []byte("not-existing")) + s.Require().NoError(err) + s.Require().False(response) +} + +func (s *ClientSuite) TestShouldRefreshToken() { + key1, err := crypto.GenerateKey() + s.Require().NoError(err) + key2, err := crypto.GenerateKey() + s.Require().NoError(err) + key3, err := crypto.GenerateKey() + s.Require().NoError(err) + key4, err := crypto.GenerateKey() + s.Require().NoError(err) + + // Contacts are added + s.Require().False(s.client.shouldRefreshToken([]*ecdsa.PublicKey{&key1.PublicKey, &key2.PublicKey}, []*ecdsa.PublicKey{&key1.PublicKey, &key2.PublicKey, &key3.PublicKey, &key4.PublicKey})) + + // everything the same + s.Require().False(s.client.shouldRefreshToken([]*ecdsa.PublicKey{&key1.PublicKey, &key2.PublicKey}, []*ecdsa.PublicKey{&key2.PublicKey, &key1.PublicKey})) + + // A contact is removed + s.Require().True(s.client.shouldRefreshToken([]*ecdsa.PublicKey{&key1.PublicKey, &key2.PublicKey}, []*ecdsa.PublicKey{&key2.PublicKey})) + +} diff --git a/protocol/pushnotificationclient/migrations/migrations.go b/protocol/pushnotificationclient/migrations/migrations.go new file mode 100644 index 000000000..e6d7b022b --- /dev/null +++ b/protocol/pushnotificationclient/migrations/migrations.go @@ -0,0 +1,319 @@ +// Code generated by go-bindata. DO NOT EDIT. +// sources: +// 1593601729_initial_schema.down.sql (144B) +// 1593601729_initial_schema.up.sql (1.773kB) +// doc.go (382B) + +package migrations + +import ( + "bytes" + "compress/gzip" + "crypto/sha256" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo + digest [sha256.Size]byte +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var __1593601729_initial_schemaDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x28\x28\x2d\xce\x88\xcf\xcb\x2f\xc9\x4c\xcb\x4c\x4e\x2c\xc9\xcc\xcf\x8b\x4f\xce\xc9\x4c\xcd\x2b\x89\x2f\x4e\x2d\x2a\x4b\x2d\x2a\xb6\xe6\x22\x46\x71\x66\x5e\x5a\x3e\x54\xa5\xa7\x9f\x8b\x6b\x84\x42\x66\x4a\x45\x3c\x5e\xd5\xf1\x05\xa5\x49\x39\x99\xc9\xf1\xd9\xa9\x95\xd6\x5c\x80\x00\x00\x00\xff\xff\x6d\xb4\xf8\x65\x90\x00\x00\x00") + +func _1593601729_initial_schemaDownSqlBytes() ([]byte, error) { + return bindataRead( + __1593601729_initial_schemaDownSql, + "1593601729_initial_schema.down.sql", + ) +} + +func _1593601729_initial_schemaDownSql() (*asset, error) { + bytes, err := _1593601729_initial_schemaDownSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "1593601729_initial_schema.down.sql", size: 144, mode: os.FileMode(0644), modTime: time.Unix(1595832279, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xa, 0x95, 0x55, 0x64, 0x38, 0x40, 0x16, 0xbf, 0x8b, 0x1c, 0x18, 0xb4, 0xc5, 0x7f, 0xd0, 0xb8, 0xf0, 0x3c, 0xa2, 0x82, 0xf8, 0x8d, 0x5a, 0xd3, 0xb6, 0x6e, 0xa3, 0xb4, 0xc, 0x9, 0x33, 0x0}} + return a, nil +} + +var __1593601729_initial_schemaUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x54\xc1\x8e\xdb\x20\x10\xbd\xfb\x2b\xe6\xb8\x91\x72\xe8\x7d\x4f\x4e\x96\x54\x96\x10\x6e\x13\x22\xe5\x86\x28\x9e\x5d\xa3\xb8\x78\x0b\x78\xd5\xfc\x7d\x85\x9d\x78\x93\xc5\xc5\x55\xba\x17\x4b\x1e\x1e\xa3\x79\x6f\x1e\x6f\xbd\x25\x39\x27\xc0\xf3\x15\x25\x50\x6c\x80\x95\x1c\xc8\xa1\xd8\xf1\x1d\xbc\x76\xae\x16\xa6\xf5\xfa\x59\x2b\xe9\x75\x6b\x84\x6a\x34\x1a\x2f\x1c\xda\x37\xb4\x0e\x1e\x32\x80\xd7\xee\x47\xa3\x95\x38\xe2\x09\x56\xb4\x5c\xf5\xf7\xd9\x9e\xd2\x65\x06\x60\xf1\x45\x3b\x8f\x16\x2b\x58\x95\x25\x25\x39\x83\x27\xb2\xc9\xf7\x94\xc3\x26\xa7\x3b\x72\x8b\x11\xd2\x43\xc1\xf8\xd8\x61\xc4\x7e\x09\xb8\x46\x3a\x2f\x2c\x7a\xab\xe7\x90\x01\x74\x12\xaa\xed\x4c\x0a\x25\x95\x42\xe7\x84\x6f\x8f\x68\x80\x93\x03\x0f\xc5\x3d\x2b\xbe\xef\xc9\xc3\x3b\xa7\x05\x94\x0c\xd6\x25\xdb\xd0\x62\xcd\x61\x4b\xbe\xd1\x7c\x4d\xb2\xc5\x63\x96\xdd\xa3\xdb\xaf\x0e\xad\xc6\x79\xdd\x06\x5c\x44\xf3\x72\x74\x12\xba\x8a\x2f\x45\xb3\x2f\x2f\xd8\xcf\x25\xa1\xcd\x73\x3b\xcb\x60\x70\x88\x48\x41\xb4\x71\x5e\x36\xcd\xd0\x5b\x57\xfd\x0e\x6e\x00\xd1\x86\x3e\x78\x2b\x58\xe1\x6d\x5a\xa5\xe0\x4e\xdd\x9a\xa8\x1e\x6b\xf4\x71\x8c\x65\x3c\xfa\xe7\xca\xe7\xad\x54\x47\xac\xc4\x4f\x74\x4e\xbe\x9c\xcd\x70\xfe\x99\xdc\xab\xaa\xa5\x9f\xd4\xe7\xd2\x69\x82\xff\x99\xe7\x7b\xdb\x5b\x0e\xc5\x57\x56\x6e\x49\x06\x70\x2f\x09\x17\x3e\xd7\x07\xf3\x34\x52\x56\xa8\xa5\xab\xb1\xfa\x3f\xb7\xf4\xf9\x30\x99\x0e\xff\x9e\x09\xae\xeb\x2d\x37\x86\x55\x84\x1a\x53\x0b\xad\x6d\x6d\xa2\x53\xb4\x80\x25\x24\x4c\x37\xed\xb0\xfb\xd7\x33\x64\xaa\xbd\xda\xcc\x25\x67\x87\x5a\x2c\x2f\x80\x6a\x8d\x97\x2a\x38\xcd\xf5\xc7\x43\xd5\x9d\x8c\xaf\xd1\x6b\x15\x34\xff\x3b\xdd\x91\xf0\x35\x7e\xf6\xdd\x14\xec\x89\x1c\x40\x57\xbf\x45\x32\x6c\xae\x7d\x51\xb2\x74\x30\xa5\x9e\xf6\xe2\x31\xfb\x13\x00\x00\xff\xff\xfb\x06\xc2\x3d\xed\x06\x00\x00") + +func _1593601729_initial_schemaUpSqlBytes() ([]byte, error) { + return bindataRead( + __1593601729_initial_schemaUpSql, + "1593601729_initial_schema.up.sql", + ) +} + +func _1593601729_initial_schemaUpSql() (*asset, error) { + bytes, err := _1593601729_initial_schemaUpSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "1593601729_initial_schema.up.sql", size: 1773, mode: os.FileMode(0644), modTime: time.Unix(1595832279, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x4e, 0x1e, 0x5, 0x35, 0x9, 0xb2, 0x2d, 0x6f, 0x33, 0x63, 0xa2, 0x7a, 0x5b, 0xd2, 0x2d, 0xcb, 0x79, 0x7e, 0x6, 0xb4, 0x9d, 0x35, 0xd8, 0x9b, 0x55, 0xe5, 0xf8, 0x44, 0xca, 0xa6, 0xf3, 0xd3}} + return a, nil +} + +var _docGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x8f\x3d\x6e\xec\x30\x0c\x84\x7b\x9d\x62\xb0\xcd\x36\xcf\x52\xf3\xaa\x74\x29\xd3\xe7\x02\x5c\x89\x96\x88\xb5\x24\x43\xa4\xf7\xe7\xf6\x81\x37\x01\xe2\x2e\xed\x87\xf9\x86\xc3\x10\xf0\x59\x44\x31\xcb\xc2\x10\x45\xe3\xc8\xaa\x34\x9e\xb8\x70\xa4\x4d\x19\xa7\x2c\x56\xb6\x8b\x8f\xbd\x06\x35\xb2\x4d\x27\xa9\xa1\x4a\x1e\x64\x1c\x6e\xff\x4f\x2e\x04\x44\x6a\x67\x43\xa1\x96\x16\x7e\x75\x29\xd4\x68\x98\xb4\x8c\xbb\x58\x01\x61\x1d\x3c\xcb\xc3\xe3\xdd\xb0\x30\xa9\xc1\x0a\xd9\x59\x61\x85\x11\x49\x79\xaf\x99\xfb\x40\xee\xd3\x45\x5a\x22\x23\xbf\xa3\x8f\xf9\x40\xf6\x85\x91\x96\x85\x13\xe6\xd1\xeb\xcb\x55\xaa\x8c\x24\x83\xa3\xf5\xf1\xfc\x07\x52\x65\x43\xa3\xca\xba\xfb\x85\x6e\x8c\xd6\x7f\xce\x83\x5a\xfa\xfb\x23\xdc\xfb\xb8\x2a\x48\xc1\x8f\x95\xa3\x71\xf2\xce\xad\x14\xaf\x94\x19\xdf\x39\xe9\x4d\x9d\x0b\x21\xf7\xb7\xcc\x8d\x77\xf3\xb8\x73\x5a\xaf\xf9\x90\xc4\xd4\xe1\x7d\xf8\x05\x3e\x77\xf8\xe0\xbe\x02\x00\x00\xff\xff\x4d\x1d\x5d\x50\x7e\x01\x00\x00") + +func docGoBytes() ([]byte, error) { + return bindataRead( + _docGo, + "doc.go", + ) +} + +func docGo() (*asset, error) { + bytes, err := docGoBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "doc.go", size: 382, mode: os.FileMode(0644), modTime: time.Unix(1595832279, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xc0, 0x2f, 0x1e, 0x64, 0x9, 0x93, 0xe4, 0x8b, 0xf2, 0x98, 0x5a, 0x45, 0xe2, 0x80, 0x88, 0x67, 0x7a, 0x2d, 0xd7, 0x4b, 0xd1, 0x73, 0xb6, 0x6d, 0x15, 0xc2, 0x0, 0x34, 0xcd, 0xa0, 0xdb, 0x20}} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + canonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[canonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// AssetString returns the asset contents as a string (instead of a []byte). +func AssetString(name string) (string, error) { + data, err := Asset(name) + return string(data), err +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// MustAssetString is like AssetString but panics when Asset would return an +// error. It simplifies safe initialization of global variables. +func MustAssetString(name string) string { + return string(MustAsset(name)) +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + canonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[canonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetDigest returns the digest of the file with the given name. It returns an +// error if the asset could not be found or the digest could not be loaded. +func AssetDigest(name string) ([sha256.Size]byte, error) { + canonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[canonicalName]; ok { + a, err := f() + if err != nil { + return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err) + } + return a.digest, nil + } + return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name) +} + +// Digests returns a map of all known files and their checksums. +func Digests() (map[string][sha256.Size]byte, error) { + mp := make(map[string][sha256.Size]byte, len(_bindata)) + for name := range _bindata { + a, err := _bindata[name]() + if err != nil { + return nil, err + } + mp[name] = a.digest + } + return mp, nil +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "1593601729_initial_schema.down.sql": _1593601729_initial_schemaDownSql, + + "1593601729_initial_schema.up.sql": _1593601729_initial_schemaUpSql, + + "doc.go": docGo, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"}, +// AssetDir("data/img") would return []string{"a.png", "b.png"}, +// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + canonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(canonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} + +var _bintree = &bintree{nil, map[string]*bintree{ + "1593601729_initial_schema.down.sql": &bintree{_1593601729_initial_schemaDownSql, map[string]*bintree{}}, + "1593601729_initial_schema.up.sql": &bintree{_1593601729_initial_schemaUpSql, map[string]*bintree{}}, + "doc.go": &bintree{docGo, map[string]*bintree{}}, +}} + +// RestoreAsset restores an asset under the given directory. +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) +} + +// RestoreAssets restores an asset under the given directory recursively. +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + canonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) +} diff --git a/protocol/pushnotificationclient/migrations/sql/1593601729_initial_schema.down.sql b/protocol/pushnotificationclient/migrations/sql/1593601729_initial_schema.down.sql new file mode 100644 index 000000000..f0e2c5fa5 --- /dev/null +++ b/protocol/pushnotificationclient/migrations/sql/1593601729_initial_schema.down.sql @@ -0,0 +1,3 @@ +DROP TABLE push_notification_client_servers; +DROP TABLE push_notification_client_info; +DROP INDEX idx_push_notification_client_info_public_key; diff --git a/protocol/pushnotificationclient/migrations/sql/1593601729_initial_schema.up.sql b/protocol/pushnotificationclient/migrations/sql/1593601729_initial_schema.up.sql new file mode 100644 index 000000000..5fa9c7a29 --- /dev/null +++ b/protocol/pushnotificationclient/migrations/sql/1593601729_initial_schema.up.sql @@ -0,0 +1,54 @@ +CREATE TABLE IF NOT EXISTS push_notification_client_servers ( + public_key BLOB NOT NULL, + registered BOOLEAN DEFAULT FALSE, + registered_at INT NOT NULL DEFAULT 0, + last_retried_at INT NOT NULL DEFAULT 0, + retry_count INT NOT NULL DEFAULT 0, + access_token TEXT, + UNIQUE(public_key) ON CONFLICT REPLACE +); + +CREATE TABLE IF NOT EXISTS push_notification_client_queries ( + public_key BLOB NOT NULL, + queried_at INT NOT NULL, + query_id BLOB NOT NULL, + UNIQUE(public_key,query_id) ON CONFLICT REPLACE +); + +CREATE TABLE IF NOT EXISTS push_notification_client_info ( + public_key BLOB NOT NULL, + server_public_key BLOB NOT NULL, + installation_id TEXT NOT NULL, + access_token TEXT NOT NULL, + retrieved_at INT NOT NULL, + version INT NOT NULL, + UNIQUE(public_key, installation_id, server_public_key) ON CONFLICT REPLACE +); + +CREATE TABLE IF NOT EXISTS push_notification_client_tracked_messages ( + message_id BLOB NOT NULL, + chat_id TEXT NOT NULL, + tracked_at INT NOT NULL, + UNIQUE(message_id) ON CONFLICT IGNORE + ); + +CREATE TABLE IF NOT EXISTS push_notification_client_sent_notifications ( + message_id BLOB NOT NULL, + public_key BLOB NOT NULL, + hashed_public_key BLOB NOT NULL, + installation_id TEXT NOT NULL, + last_tried_at INT NOT NULL, + retry_count INT NOT NULL DEFAULT 0, + success BOOLEAN NOT NULL DEFAULT FALSE, + error INT NOT NULL DEFAULT 0, + UNIQUE(message_id, public_key, installation_id) ON CONFLICT REPLACE + ); + +CREATE TABLE IF NOT EXISTS push_notification_client_registrations ( + registration BLOB NOT NULL, + contact_ids BLOB, + synthetic_id INT NOT NULL DEFAULT 0, + UNIQUE(synthetic_id) ON CONFLICT REPLACE +); + +CREATE INDEX idx_push_notification_client_info_public_key ON push_notification_client_info(public_key, installation_id); diff --git a/protocol/pushnotificationclient/migrations/sql/doc.go b/protocol/pushnotificationclient/migrations/sql/doc.go new file mode 100644 index 000000000..a7d080561 --- /dev/null +++ b/protocol/pushnotificationclient/migrations/sql/doc.go @@ -0,0 +1,9 @@ +// This file is necessary because "github.com/status-im/migrate/v4" +// can't handle files starting with a prefix. At least that's the case +// for go-bindata. +// If go-bindata is called from the same directory, asset names +// have no prefix and "github.com/status-im/migrate/v4" works as expected. + +package migrations + +//go:generate go-bindata -pkg migrations -o ../migrations.go ./ diff --git a/protocol/pushnotificationclient/persistence.go b/protocol/pushnotificationclient/persistence.go new file mode 100644 index 000000000..5a49639f0 --- /dev/null +++ b/protocol/pushnotificationclient/persistence.go @@ -0,0 +1,396 @@ +package pushnotificationclient + +import ( + "bytes" + "context" + "crypto/ecdsa" + "database/sql" + "encoding/gob" + "strings" + "time" + + "github.com/golang/protobuf/proto" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/protocol/protobuf" +) + +type Persistence struct { + db *sql.DB +} + +func NewPersistence(db *sql.DB) *Persistence { + return &Persistence{db: db} +} + +func (p *Persistence) GetLastPushNotificationRegistration() (*protobuf.PushNotificationRegistration, []*ecdsa.PublicKey, error) { + var registrationBytes []byte + var contactIDsBytes []byte + err := p.db.QueryRow(`SELECT registration,contact_ids FROM push_notification_client_registrations LIMIT 1`).Scan(®istrationBytes, &contactIDsBytes) + if err == sql.ErrNoRows { + return nil, nil, nil + } else if err != nil { + return nil, nil, err + } + + var publicKeyBytes [][]byte + var contactIDs []*ecdsa.PublicKey + // Restore contactIDs + contactIDsDecoder := gob.NewDecoder(bytes.NewBuffer(contactIDsBytes)) + err = contactIDsDecoder.Decode(&publicKeyBytes) + if err != nil { + return nil, nil, err + } + for _, pkBytes := range publicKeyBytes { + pk, err := crypto.DecompressPubkey(pkBytes) + if err != nil { + return nil, nil, err + } + contactIDs = append(contactIDs, pk) + } + + registration := &protobuf.PushNotificationRegistration{} + + err = proto.Unmarshal(registrationBytes, registration) + if err != nil { + return nil, nil, err + } + + return registration, contactIDs, nil +} + +func (p *Persistence) SaveLastPushNotificationRegistration(registration *protobuf.PushNotificationRegistration, contactIDs []*ecdsa.PublicKey) error { + var encodedContactIDs bytes.Buffer + var contactIDsBytes [][]byte + for _, pk := range contactIDs { + contactIDsBytes = append(contactIDsBytes, crypto.CompressPubkey(pk)) + } + pkEncoder := gob.NewEncoder(&encodedContactIDs) + if err := pkEncoder.Encode(contactIDsBytes); err != nil { + return err + } + + marshaledRegistration, err := proto.Marshal(registration) + if err != nil { + return err + } + _, err = p.db.Exec(`INSERT INTO push_notification_client_registrations (registration,contact_ids) VALUES (?, ?)`, marshaledRegistration, encodedContactIDs.Bytes()) + return err +} + +func (p *Persistence) TrackPushNotification(chatID string, messageID []byte) error { + trackedAt := time.Now().Unix() + _, err := p.db.Exec(`INSERT INTO push_notification_client_tracked_messages (chat_id, message_id, tracked_at) VALUES (?,?,?)`, chatID, messageID, trackedAt) + return err +} + +func (p *Persistence) TrackedMessage(messageID []byte) (bool, error) { + var count uint64 + err := p.db.QueryRow(`SELECT COUNT(1) FROM push_notification_client_tracked_messages WHERE message_id = ?`, messageID).Scan(&count) + if err != nil { + return false, err + } + + if count == 0 { + return false, nil + } + + return true, nil +} + +func (p *Persistence) SavePushNotificationQuery(publicKey *ecdsa.PublicKey, queryID []byte) error { + queriedAt := time.Now().Unix() + _, err := p.db.Exec(`INSERT INTO push_notification_client_queries (public_key, query_id, queried_at) VALUES (?,?,?)`, crypto.CompressPubkey(publicKey), queryID, queriedAt) + return err +} + +func (p *Persistence) GetQueriedAt(publicKey *ecdsa.PublicKey) (int64, error) { + var queriedAt int64 + err := p.db.QueryRow(`SELECT queried_at FROM push_notification_client_queries WHERE public_key = ? ORDER BY queried_at DESC LIMIT 1`, crypto.CompressPubkey(publicKey)).Scan(&queriedAt) + if err == sql.ErrNoRows { + return 0, nil + } + if err != nil { + return 0, err + } + + return queriedAt, nil +} + +func (p *Persistence) GetQueryPublicKey(queryID []byte) (*ecdsa.PublicKey, error) { + var publicKeyBytes []byte + err := p.db.QueryRow(`SELECT public_key FROM push_notification_client_queries WHERE query_id = ?`, queryID).Scan(&publicKeyBytes) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + publicKey, err := crypto.DecompressPubkey(publicKeyBytes) + if err != nil { + return nil, err + } + return publicKey, nil +} + +func (p *Persistence) SavePushNotificationInfo(infos []*PushNotificationInfo) error { + tx, err := p.db.BeginTx(context.Background(), &sql.TxOptions{}) + defer func() { + if err == nil { + err = tx.Commit() + return + } + // don't shadow original error + _ = tx.Rollback() + }() + for _, info := range infos { + var latestVersion uint64 + clientCompressedKey := crypto.CompressPubkey(info.PublicKey) + err := tx.QueryRow(`SELECT IFNULL(MAX(version),0) FROM push_notification_client_info WHERE public_key = ? AND installation_id = ? LIMIT 1`, clientCompressedKey, info.InstallationID).Scan(&latestVersion) + if err != sql.ErrNoRows && err != nil { + return err + } + if latestVersion > info.Version { + // Nothing to do + continue + } + + // Remove anything that as a lower version + _, err = tx.Exec(`DELETE FROM push_notification_client_info WHERE public_key = ? AND installation_id = ? AND version < ?`, clientCompressedKey, info.InstallationID, info.Version) + if err != nil { + return err + } + // Insert + _, err = tx.Exec(`INSERT INTO push_notification_client_info (public_key, server_public_key, installation_id, access_token, retrieved_at, version) VALUES (?, ?, ?, ?, ?,?)`, clientCompressedKey, crypto.CompressPubkey(info.ServerPublicKey), info.InstallationID, info.AccessToken, info.RetrievedAt, info.Version) + if err != nil { + return err + } + } + + return nil +} + +func (p *Persistence) GetPushNotificationInfo(publicKey *ecdsa.PublicKey, installationIDs []string) ([]*PushNotificationInfo, error) { + queryArgs := make([]interface{}, 0, len(installationIDs)+1) + queryArgs = append(queryArgs, crypto.CompressPubkey(publicKey)) + for _, installationID := range installationIDs { + queryArgs = append(queryArgs, installationID) + } + + inVector := strings.Repeat("?, ", len(installationIDs)-1) + "?" + + rows, err := p.db.Query(`SELECT server_public_key, installation_id, version, access_token, retrieved_at FROM push_notification_client_info WHERE public_key = ? AND installation_id IN (`+inVector+`)`, queryArgs...) //nolint: gosec + + if err != nil { + return nil, err + } + defer rows.Close() + + var infos []*PushNotificationInfo + for rows.Next() { + var serverPublicKeyBytes []byte + info := &PushNotificationInfo{PublicKey: publicKey} + err := rows.Scan(&serverPublicKeyBytes, &info.InstallationID, &info.Version, &info.AccessToken, &info.RetrievedAt) + if err != nil { + return nil, err + } + + serverPublicKey, err := crypto.DecompressPubkey(serverPublicKeyBytes) + if err != nil { + return nil, err + } + + info.ServerPublicKey = serverPublicKey + infos = append(infos, info) + } + + return infos, nil +} + +func (p *Persistence) GetPushNotificationInfoByPublicKey(publicKey *ecdsa.PublicKey) ([]*PushNotificationInfo, error) { + rows, err := p.db.Query(`SELECT server_public_key, installation_id, access_token, retrieved_at FROM push_notification_client_info WHERE public_key = ?`, crypto.CompressPubkey(publicKey)) + if err != nil { + return nil, err + } + defer rows.Close() + + var infos []*PushNotificationInfo + for rows.Next() { + var serverPublicKeyBytes []byte + info := &PushNotificationInfo{PublicKey: publicKey} + err := rows.Scan(&serverPublicKeyBytes, &info.InstallationID, &info.AccessToken, &info.RetrievedAt) + if err != nil { + return nil, err + } + + serverPublicKey, err := crypto.DecompressPubkey(serverPublicKeyBytes) + if err != nil { + return nil, err + } + + info.ServerPublicKey = serverPublicKey + infos = append(infos, info) + } + + return infos, nil +} + +func (p *Persistence) ShouldSendNotificationFor(publicKey *ecdsa.PublicKey, installationID string, messageID []byte) (bool, error) { + // First we check that we are tracking this message, next we check that we haven't already sent this + var count uint64 + err := p.db.QueryRow(`SELECT COUNT(1) FROM push_notification_client_tracked_messages WHERE message_id = ?`, messageID).Scan(&count) + if err != nil { + return false, err + } + + if count == 0 { + return false, nil + } + + err = p.db.QueryRow(`SELECT COUNT(1) FROM push_notification_client_sent_notifications WHERE message_id = ? AND public_key = ? AND installation_id = ? `, messageID, crypto.CompressPubkey(publicKey), installationID).Scan(&count) + if err != nil { + return false, err + } + + return count == 0, nil +} + +func (p *Persistence) ShouldSendNotificationToAllInstallationIDs(publicKey *ecdsa.PublicKey, messageID []byte) (bool, error) { + // First we check that we are tracking this message, next we check that we haven't already sent this + var count uint64 + err := p.db.QueryRow(`SELECT COUNT(1) FROM push_notification_client_tracked_messages WHERE message_id = ?`, messageID).Scan(&count) + if err != nil { + return false, err + } + + if count == 0 { + return false, nil + } + + err = p.db.QueryRow(`SELECT COUNT(1) FROM push_notification_client_sent_notifications WHERE message_id = ? AND public_key = ? `, messageID, crypto.CompressPubkey(publicKey)).Scan(&count) + if err != nil { + return false, err + } + + return count == 0, nil +} + +func (p *Persistence) UpsertSentNotification(n *SentNotification) error { + _, err := p.db.Exec(`INSERT INTO push_notification_client_sent_notifications (public_key, installation_id, message_id, last_tried_at, retry_count, success, error, hashed_public_key) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, crypto.CompressPubkey(n.PublicKey), n.InstallationID, n.MessageID, n.LastTriedAt, n.RetryCount, n.Success, n.Error, n.HashedPublicKey()) + return err +} + +func (p *Persistence) GetSentNotification(hashedPublicKey []byte, installationID string, messageID []byte) (*SentNotification, error) { + var publicKeyBytes []byte + sentNotification := &SentNotification{ + InstallationID: installationID, + MessageID: messageID, + } + err := p.db.QueryRow(`SELECT retry_count, last_tried_at, error, success, public_key FROM push_notification_client_sent_notifications WHERE hashed_public_key = ?`, hashedPublicKey).Scan(&sentNotification.RetryCount, &sentNotification.LastTriedAt, &sentNotification.Error, &sentNotification.Success, &publicKeyBytes) + if err != nil { + return nil, err + } + + publicKey, err := crypto.DecompressPubkey(publicKeyBytes) + if err != nil { + return nil, err + } + + sentNotification.PublicKey = publicKey + + return sentNotification, nil +} + +func (p *Persistence) UpdateNotificationResponse(messageID []byte, response *protobuf.PushNotificationReport) error { + _, err := p.db.Exec(`UPDATE push_notification_client_sent_notifications SET success = ?, error = ? WHERE hashed_public_key = ? AND installation_id = ? AND message_id = ? AND NOT success`, response.Success, response.Error, response.PublicKey, response.InstallationId, messageID) + return err +} + +func (p *Persistence) GetRetriablePushNotifications() ([]*SentNotification, error) { + var notifications []*SentNotification + rows, err := p.db.Query(`SELECT retry_count, last_tried_at, error, success, public_key, installation_id, message_id FROM push_notification_client_sent_notifications WHERE NOT success AND error = ?`, protobuf.PushNotificationReport_WRONG_TOKEN) + if err != nil { + return nil, err + } + defer rows.Close() + + for rows.Next() { + var publicKeyBytes []byte + notification := &SentNotification{} + err = rows.Scan(¬ification.RetryCount, ¬ification.LastTriedAt, ¬ification.Error, ¬ification.Success, &publicKeyBytes, ¬ification.InstallationID, ¬ification.MessageID) + if err != nil { + return nil, err + } + publicKey, err := crypto.DecompressPubkey(publicKeyBytes) + if err != nil { + return nil, err + } + notification.PublicKey = publicKey + notifications = append(notifications, notification) + } + return notifications, err +} + +func (p *Persistence) UpsertServer(server *PushNotificationServer) error { + _, err := p.db.Exec(`INSERT INTO push_notification_client_servers (public_key, registered, registered_at, access_token, last_retried_at, retry_count) VALUES (?,?,?,?,?,?)`, crypto.CompressPubkey(server.PublicKey), server.Registered, server.RegisteredAt, server.AccessToken, server.LastRetriedAt, server.RetryCount) + return err + +} + +func (p *Persistence) GetServers() ([]*PushNotificationServer, error) { + rows, err := p.db.Query(`SELECT public_key, registered, registered_at,access_token,last_retried_at, retry_count FROM push_notification_client_servers`) + if err != nil { + return nil, err + } + defer rows.Close() + + var servers []*PushNotificationServer + for rows.Next() { + server := &PushNotificationServer{} + var key []byte + err := rows.Scan(&key, &server.Registered, &server.RegisteredAt, &server.AccessToken, &server.LastRetriedAt, &server.RetryCount) + if err != nil { + return nil, err + } + parsedKey, err := crypto.DecompressPubkey(key) + if err != nil { + return nil, err + } + server.PublicKey = parsedKey + servers = append(servers, server) + } + return servers, nil +} + +func (p *Persistence) GetServersByPublicKey(keys []*ecdsa.PublicKey) ([]*PushNotificationServer, error) { + + keyArgs := make([]interface{}, 0, len(keys)) + for _, key := range keys { + keyArgs = append(keyArgs, crypto.CompressPubkey(key)) + } + + inVector := strings.Repeat("?, ", len(keys)-1) + "?" + rows, err := p.db.Query(`SELECT public_key, registered, registered_at,access_token FROM push_notification_client_servers WHERE public_key IN (`+inVector+")", keyArgs...) //nolint: gosec + if err != nil { + return nil, err + } + defer rows.Close() + + var servers []*PushNotificationServer + for rows.Next() { + server := &PushNotificationServer{} + var key []byte + err := rows.Scan(&key, &server.Registered, &server.RegisteredAt, &server.AccessToken) + if err != nil { + return nil, err + } + parsedKey, err := crypto.DecompressPubkey(key) + if err != nil { + return nil, err + } + server.PublicKey = parsedKey + servers = append(servers, server) + } + return servers, nil +} diff --git a/protocol/pushnotificationclient/persistence_test.go b/protocol/pushnotificationclient/persistence_test.go new file mode 100644 index 000000000..cdb401b43 --- /dev/null +++ b/protocol/pushnotificationclient/persistence_test.go @@ -0,0 +1,334 @@ +package pushnotificationclient + +import ( + "crypto/ecdsa" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/suite" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/sqlite" +) + +const ( + testAccessToken = "token" + installationID1 = "installation-id-1" + installationID2 = "installation-id-2" + installationID3 = "installation-id-3" +) + +func TestSQLitePersistenceSuite(t *testing.T) { + suite.Run(t, new(SQLitePersistenceSuite)) +} + +type SQLitePersistenceSuite struct { + suite.Suite + tmpFile *os.File + persistence *Persistence +} + +func (s *SQLitePersistenceSuite) SetupTest() { + tmpFile, err := ioutil.TempFile("", "") + s.Require().NoError(err) + s.tmpFile = tmpFile + + database, err := sqlite.Open(s.tmpFile.Name(), "") + s.Require().NoError(err) + s.persistence = NewPersistence(database) +} + +func (s *SQLitePersistenceSuite) TearDownTest() { + _ = os.Remove(s.tmpFile.Name()) +} + +func (s *SQLitePersistenceSuite) TestSaveAndRetrieveServer() { + key, err := crypto.GenerateKey() + s.Require().NoError(err) + + server := &PushNotificationServer{ + PublicKey: &key.PublicKey, + Registered: true, + RegisteredAt: 1, + AccessToken: testAccessToken, + } + + s.Require().NoError(s.persistence.UpsertServer(server)) + + retrievedServers, err := s.persistence.GetServers() + s.Require().NoError(err) + + s.Require().Len(retrievedServers, 1) + s.Require().True(retrievedServers[0].Registered) + s.Require().Equal(int64(1), retrievedServers[0].RegisteredAt) + s.Require().True(common.IsPubKeyEqual(retrievedServers[0].PublicKey, &key.PublicKey)) + s.Require().Equal(testAccessToken, retrievedServers[0].AccessToken) + + server.Registered = false + server.RegisteredAt = 2 + + s.Require().NoError(s.persistence.UpsertServer(server)) + + retrievedServers, err = s.persistence.GetServers() + s.Require().NoError(err) + + s.Require().Len(retrievedServers, 1) + s.Require().False(retrievedServers[0].Registered) + s.Require().Equal(int64(2), retrievedServers[0].RegisteredAt) + s.Require().True(common.IsPubKeyEqual(retrievedServers[0].PublicKey, &key.PublicKey)) +} + +func (s *SQLitePersistenceSuite) TestSaveAndRetrieveInfo() { + key1, err := crypto.GenerateKey() + s.Require().NoError(err) + key2, err := crypto.GenerateKey() + s.Require().NoError(err) + serverKey, err := crypto.GenerateKey() + s.Require().NoError(err) + + infos := []*PushNotificationInfo{ + { + PublicKey: &key1.PublicKey, + ServerPublicKey: &serverKey.PublicKey, + RetrievedAt: 1, + Version: 1, + AccessToken: testAccessToken, + InstallationID: installationID1, + }, + { + PublicKey: &key1.PublicKey, + ServerPublicKey: &serverKey.PublicKey, + RetrievedAt: 1, + Version: 1, + AccessToken: testAccessToken, + InstallationID: installationID2, + }, + { + PublicKey: &key1.PublicKey, + ServerPublicKey: &serverKey.PublicKey, + RetrievedAt: 1, + Version: 1, + AccessToken: testAccessToken, + InstallationID: installationID3, + }, + { + PublicKey: &key2.PublicKey, + ServerPublicKey: &serverKey.PublicKey, + RetrievedAt: 1, + Version: 1, + AccessToken: testAccessToken, + InstallationID: installationID1, + }, + { + PublicKey: &key2.PublicKey, + ServerPublicKey: &serverKey.PublicKey, + RetrievedAt: 1, + Version: 1, + AccessToken: testAccessToken, + InstallationID: installationID2, + }, + { + PublicKey: &key2.PublicKey, + ServerPublicKey: &serverKey.PublicKey, + RetrievedAt: 1, + Version: 1, + AccessToken: testAccessToken, + InstallationID: installationID3, + }, + } + + s.Require().NoError(s.persistence.SavePushNotificationInfo(infos)) + + retrievedInfos, err := s.persistence.GetPushNotificationInfo(&key1.PublicKey, []string{installationID1, installationID2}) + s.Require().NoError(err) + + s.Require().Len(retrievedInfos, 2) +} + +func (s *SQLitePersistenceSuite) TestSaveAndRetrieveInfoWithVersion() { + installationID := "installation-id-1" + key, err := crypto.GenerateKey() + s.Require().NoError(err) + serverKey1, err := crypto.GenerateKey() + s.Require().NoError(err) + serverKey2, err := crypto.GenerateKey() + s.Require().NoError(err) + + infos := []*PushNotificationInfo{ + { + PublicKey: &key.PublicKey, + ServerPublicKey: &serverKey1.PublicKey, + RetrievedAt: 1, + Version: 1, + AccessToken: testAccessToken, + InstallationID: installationID, + }, + { + PublicKey: &key.PublicKey, + ServerPublicKey: &serverKey2.PublicKey, + RetrievedAt: 1, + Version: 1, + AccessToken: testAccessToken, + InstallationID: installationID, + }, + } + + s.Require().NoError(s.persistence.SavePushNotificationInfo(infos)) + + retrievedInfos, err := s.persistence.GetPushNotificationInfo(&key.PublicKey, []string{installationID}) + s.Require().NoError(err) + + // We should retrieve both + s.Require().Len(retrievedInfos, 2) + s.Require().Equal(uint64(1), retrievedInfos[0].Version) + + // Bump version + infos[0].Version = 2 + + s.Require().NoError(s.persistence.SavePushNotificationInfo(infos)) + + retrievedInfos, err = s.persistence.GetPushNotificationInfo(&key.PublicKey, []string{installationID}) + s.Require().NoError(err) + + // Only one should be retrieved now + s.Require().Len(retrievedInfos, 1) + s.Require().Equal(uint64(2), retrievedInfos[0].Version) + + // Lower version + infos[0].Version = 1 + + s.Require().NoError(s.persistence.SavePushNotificationInfo(infos)) + + retrievedInfos, err = s.persistence.GetPushNotificationInfo(&key.PublicKey, []string{installationID}) + s.Require().NoError(err) + + s.Require().Len(retrievedInfos, 1) + s.Require().Equal(uint64(2), retrievedInfos[0].Version) +} + +func (s *SQLitePersistenceSuite) TestNotifiedOnAndUpdateNotificationResponse() { + key, err := crypto.GenerateKey() + s.Require().NoError(err) + installationID := "installation-id" + messageID := []byte("message-id") + + sentNotification := &SentNotification{ + PublicKey: &key.PublicKey, + InstallationID: installationID, + MessageID: messageID, + LastTriedAt: time.Now().Unix(), + } + + s.Require().NoError(s.persistence.UpsertSentNotification(sentNotification)) + + retrievedNotification, err := s.persistence.GetSentNotification(sentNotification.HashedPublicKey(), installationID, messageID) + s.Require().NoError(err) + s.Require().Equal(sentNotification, retrievedNotification) + + retriableNotifications, err := s.persistence.GetRetriablePushNotifications() + s.Require().NoError(err) + s.Require().Len(retriableNotifications, 0) + + response := &protobuf.PushNotificationReport{ + Success: false, + Error: protobuf.PushNotificationReport_WRONG_TOKEN, + PublicKey: sentNotification.HashedPublicKey(), + InstallationId: installationID, + } + + s.Require().NoError(s.persistence.UpdateNotificationResponse(messageID, response)) + // This notification should be retriable + retriableNotifications, err = s.persistence.GetRetriablePushNotifications() + s.Require().NoError(err) + s.Require().Len(retriableNotifications, 1) + + sentNotification.Error = protobuf.PushNotificationReport_WRONG_TOKEN + + retrievedNotification, err = s.persistence.GetSentNotification(sentNotification.HashedPublicKey(), installationID, messageID) + s.Require().NoError(err) + s.Require().Equal(sentNotification, retrievedNotification) + + // Update with a successful notification + response = &protobuf.PushNotificationReport{ + Success: true, + PublicKey: sentNotification.HashedPublicKey(), + InstallationId: installationID, + } + + s.Require().NoError(s.persistence.UpdateNotificationResponse(messageID, response)) + + sentNotification.Success = true + sentNotification.Error = protobuf.PushNotificationReport_UNKNOWN_ERROR_TYPE + + retrievedNotification, err = s.persistence.GetSentNotification(sentNotification.HashedPublicKey(), installationID, messageID) + s.Require().NoError(err) + s.Require().Equal(sentNotification, retrievedNotification) + + // This notification should not be retriable + retriableNotifications, err = s.persistence.GetRetriablePushNotifications() + s.Require().NoError(err) + s.Require().Len(retriableNotifications, 0) + + // Update with a unsuccessful notification, it should be ignored + response = &protobuf.PushNotificationReport{ + Success: false, + Error: protobuf.PushNotificationReport_WRONG_TOKEN, + PublicKey: sentNotification.HashedPublicKey(), + InstallationId: installationID, + } + + s.Require().NoError(s.persistence.UpdateNotificationResponse(messageID, response)) + + sentNotification.Success = true + sentNotification.Error = protobuf.PushNotificationReport_UNKNOWN_ERROR_TYPE + + retrievedNotification, err = s.persistence.GetSentNotification(sentNotification.HashedPublicKey(), installationID, messageID) + s.Require().NoError(err) + s.Require().Equal(sentNotification, retrievedNotification) +} + +func (s *SQLitePersistenceSuite) TestSaveAndRetrieveRegistration() { + // Try with nil first + retrievedRegistration, retrievedContactIDs, err := s.persistence.GetLastPushNotificationRegistration() + s.Require().NoError(err) + s.Require().Nil(retrievedRegistration) + s.Require().Nil(retrievedContactIDs) + + // Save & retrieve registration + registration := &protobuf.PushNotificationRegistration{ + AccessToken: "test", + Version: 3, + } + + key1, err := crypto.GenerateKey() + s.Require().NoError(err) + + key2, err := crypto.GenerateKey() + s.Require().NoError(err) + + key3, err := crypto.GenerateKey() + s.Require().NoError(err) + + publicKeys := []*ecdsa.PublicKey{&key1.PublicKey, &key2.PublicKey} + + s.Require().NoError(s.persistence.SaveLastPushNotificationRegistration(registration, publicKeys)) + retrievedRegistration, retrievedContactIDs, err = s.persistence.GetLastPushNotificationRegistration() + s.Require().NoError(err) + s.Require().True(proto.Equal(registration, retrievedRegistration)) + s.Require().Equal(publicKeys, retrievedContactIDs) + + // Override and retrieve + + registration.Version = 5 + publicKeys = append(publicKeys, &key3.PublicKey) + s.Require().NoError(s.persistence.SaveLastPushNotificationRegistration(registration, publicKeys)) + retrievedRegistration, retrievedContactIDs, err = s.persistence.GetLastPushNotificationRegistration() + s.Require().NoError(err) + s.Require().True(proto.Equal(registration, retrievedRegistration)) + s.Require().Equal(publicKeys, retrievedContactIDs) +} diff --git a/protocol/pushnotificationserver/errors.go b/protocol/pushnotificationserver/errors.go new file mode 100644 index 000000000..ea9a580b3 --- /dev/null +++ b/protocol/pushnotificationserver/errors.go @@ -0,0 +1,13 @@ +package pushnotificationserver + +import "errors" + +var ErrInvalidPushNotificationRegistrationVersion = errors.New("invalid version") +var ErrEmptyPushNotificationRegistrationPayload = errors.New("empty payload") +var ErrMalformedPushNotificationRegistrationInstallationID = errors.New("invalid installationID") +var ErrEmptyPushNotificationRegistrationPublicKey = errors.New("no public key") +var ErrCouldNotUnmarshalPushNotificationRegistration = errors.New("could not unmarshal preferences") +var ErrMalformedPushNotificationRegistrationDeviceToken = errors.New("invalid device token") +var ErrMalformedPushNotificationRegistrationGrant = errors.New("invalid grant") +var ErrMalformedPushNotificationRegistrationAccessToken = errors.New("invalid access token") +var ErrUnknownPushNotificationRegistrationTokenType = errors.New("invalid token type") diff --git a/protocol/pushnotificationserver/gorush.go b/protocol/pushnotificationserver/gorush.go new file mode 100644 index 000000000..e791583b0 --- /dev/null +++ b/protocol/pushnotificationserver/gorush.go @@ -0,0 +1,77 @@ +package pushnotificationserver + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "net/http" + + "github.com/status-im/status-go/protocol/protobuf" +) + +const defaultNotificationMessage = "You have a new message" + +type GoRushRequestData struct { + EncryptedMessage string `json:"encryptedMessage"` + ChatID string `json:"chatId"` + PublicKey string `json:"publicKey"` +} + +type GoRushRequestNotification struct { + Tokens []string `json:"tokens"` + Platform uint `json:"platform"` + Message string `json:"message"` + Data *GoRushRequestData `json:"data"` +} + +type GoRushRequest struct { + Notifications []*GoRushRequestNotification `json:"notifications"` +} + +type RequestAndRegistration struct { + Request *protobuf.PushNotification + Registration *protobuf.PushNotificationRegistration +} + +func tokenTypeToGoRushPlatform(tokenType protobuf.PushNotificationRegistration_TokenType) uint { + switch tokenType { + case protobuf.PushNotificationRegistration_APN_TOKEN: + return 1 + case protobuf.PushNotificationRegistration_FIREBASE_TOKEN: + return 2 + } + return 0 +} + +func PushNotificationRegistrationToGoRushRequest(requestAndRegistrations []*RequestAndRegistration) *GoRushRequest { + goRushRequests := &GoRushRequest{} + for _, requestAndRegistration := range requestAndRegistrations { + request := requestAndRegistration.Request + registration := requestAndRegistration.Registration + goRushRequests.Notifications = append(goRushRequests.Notifications, + &GoRushRequestNotification{ + Tokens: []string{registration.DeviceToken}, + Platform: tokenTypeToGoRushPlatform(registration.TokenType), + Message: defaultNotificationMessage, + Data: &GoRushRequestData{ + EncryptedMessage: hex.EncodeToString(request.Message), + ChatID: request.ChatId, + PublicKey: hex.EncodeToString(request.PublicKey), + }, + }) + } + return goRushRequests +} + +func sendGoRushNotification(request *GoRushRequest, url string) error { + payload, err := json.Marshal(request) + if err != nil { + return err + } + + _, err = http.Post(url+"/api/push", "application/json", bytes.NewReader(payload)) + if err != nil { + return err + } + return nil +} diff --git a/protocol/pushnotificationserver/gorush_test.go b/protocol/pushnotificationserver/gorush_test.go new file mode 100644 index 000000000..f60b21a01 --- /dev/null +++ b/protocol/pushnotificationserver/gorush_test.go @@ -0,0 +1,107 @@ +package pushnotificationserver + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/status-im/status-go/protocol/protobuf" +) + +func TestPushNotificationRegistrationToGoRushRequest(t *testing.T) { + message1 := []byte("message-1") + message2 := []byte("message-2") + message3 := []byte("message-3") + hexMessage1 := hex.EncodeToString(message1) + hexMessage2 := hex.EncodeToString(message2) + hexMessage3 := hex.EncodeToString(message3) + chatID := "chat-id" + publicKey1 := []byte("public-key-1") + publicKey2 := []byte("public-key-2") + installationID1 := "installation-id-1" + installationID2 := "installation-id-2" + installationID3 := "installation-id-3" + var platform1 uint = 1 + var platform2 uint = 2 + var platform3 uint = 2 + token1 := "token-1" + token2 := "token-2" + token3 := "token-3" + + requestAndRegistrations := []*RequestAndRegistration{ + { + Request: &protobuf.PushNotification{ + ChatId: chatID, + PublicKey: publicKey1, + InstallationId: installationID1, + Message: message1, + }, + Registration: &protobuf.PushNotificationRegistration{ + DeviceToken: token1, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + }, + }, + { + Request: &protobuf.PushNotification{ + ChatId: chatID, + PublicKey: publicKey1, + InstallationId: installationID2, + Message: message2, + }, + Registration: &protobuf.PushNotificationRegistration{ + DeviceToken: token2, + TokenType: protobuf.PushNotificationRegistration_FIREBASE_TOKEN, + }, + }, + { + Request: &protobuf.PushNotification{ + ChatId: chatID, + PublicKey: publicKey2, + InstallationId: installationID3, + Message: message3, + }, + Registration: &protobuf.PushNotificationRegistration{ + DeviceToken: token3, + TokenType: protobuf.PushNotificationRegistration_FIREBASE_TOKEN, + }, + }, + } + + expectedRequests := &GoRushRequest{ + Notifications: []*GoRushRequestNotification{ + { + Tokens: []string{token1}, + Platform: platform1, + Message: defaultNotificationMessage, + Data: &GoRushRequestData{ + EncryptedMessage: hexMessage1, + ChatID: chatID, + PublicKey: hex.EncodeToString(publicKey1), + }, + }, + { + Tokens: []string{token2}, + Platform: platform2, + Message: defaultNotificationMessage, + Data: &GoRushRequestData{ + EncryptedMessage: hexMessage2, + ChatID: chatID, + PublicKey: hex.EncodeToString(publicKey1), + }, + }, + { + Tokens: []string{token3}, + Platform: platform3, + Message: defaultNotificationMessage, + Data: &GoRushRequestData{ + EncryptedMessage: hexMessage3, + ChatID: chatID, + PublicKey: hex.EncodeToString(publicKey2), + }, + }, + }, + } + actualRequests := PushNotificationRegistrationToGoRushRequest(requestAndRegistrations) + require.Equal(t, expectedRequests, actualRequests) +} diff --git a/protocol/pushnotificationserver/migrations/migrations.go b/protocol/pushnotificationserver/migrations/migrations.go new file mode 100644 index 000000000..a663903c9 --- /dev/null +++ b/protocol/pushnotificationserver/migrations/migrations.go @@ -0,0 +1,319 @@ +// Code generated by go-bindata. DO NOT EDIT. +// sources: +// 1593601728_initial_schema.down.sql (200B) +// 1593601728_initial_schema.up.sql (675B) +// doc.go (382B) + +package migrations + +import ( + "bytes" + "compress/gzip" + "crypto/sha256" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo + digest [sha256.Size]byte +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var __1593601728_initial_schemaDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\x09\xf2\x0f\x50\x08\x71\x74\xf2\x71\x55\x28\x28\x2d\xce\x88\xcf\xcb\x2f\xc9\x4c\xcb\x4c\x4e\x2c\xc9\xcc\xcf\x8b\x2f\x4e\x2d\x2a\x4b\x2d\x8a\x2f\x4a\x4d\xcf\x2c\x2e\x29\x02\x8b\x15\x5b\x73\x81\xb5\x78\xfa\xb9\xb8\x46\x28\x64\xa6\x54\xc4\x13\xa7\x2d\xbe\xa0\x34\x29\x27\x33\x39\x3e\x3b\xb5\x92\x72\x13\xe2\x33\xf3\x8a\x4b\x12\x73\x72\x20\x8a\x33\x53\xac\xb9\xb8\x00\x01\x00\x00\xff\xff\x90\x39\xe0\x1c\xc8\x00\x00\x00") + +func _1593601728_initial_schemaDownSqlBytes() ([]byte, error) { + return bindataRead( + __1593601728_initial_schemaDownSql, + "1593601728_initial_schema.down.sql", + ) +} + +func _1593601728_initial_schemaDownSql() (*asset, error) { + bytes, err := _1593601728_initial_schemaDownSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "1593601728_initial_schema.down.sql", size: 200, mode: os.FileMode(0644), modTime: time.Unix(1595832279, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x88, 0x8a, 0x61, 0x81, 0x57, 0x45, 0x9b, 0x97, 0x9b, 0x1f, 0xf6, 0x94, 0x8a, 0x20, 0xb3, 0x2b, 0xff, 0x69, 0x49, 0xf4, 0x58, 0xcc, 0xd0, 0x55, 0xcc, 0x9a, 0x8b, 0xb6, 0x7f, 0x29, 0x53, 0xc1}} + return a, nil +} + +var __1593601728_initial_schemaUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x91\x31\x6b\xc3\x30\x14\x84\x77\xfd\x8a\x37\xc6\x90\xa1\xbb\x27\xd9\x91\xa9\x40\x48\xad\x23\x97\x6c\xc2\xb5\xd5\xe6\x51\x23\x07\x49\x31\xf5\xbf\x2f\x71\x86\x2a\x69\x87\x10\xb2\x1e\x8f\xbb\xf7\xdd\x95\x35\xa3\x9a\x81\xa6\x85\x60\xc0\x2b\x90\x4a\x03\xdb\xf1\xad\xde\xc2\xe1\x18\xf6\xc6\x8d\x11\x3f\xb0\x6b\x23\x8e\xce\x04\xeb\x27\xeb\x8d\xb7\x9f\x18\xa2\x5f\xb4\x00\x2b\x02\x70\x38\xbe\x0f\xd8\x99\x2f\x3b\x43\x21\x54\xb1\xb8\xc8\x46\x88\x35\x01\x40\x17\x62\x3b\x0c\x67\x07\xec\xe1\x8d\xd6\xe5\x33\xad\x2f\x6e\x26\xeb\x03\x8e\x0e\xb8\xd4\x17\x7a\x9a\xb4\x38\x9f\xc4\x46\xf2\xd7\x86\xad\x7e\x33\xd7\xd7\x19\x19\x28\x09\xa5\x92\x95\xe0\xa5\x86\x9a\xbd\x08\x5a\x32\x92\xe5\x84\xdc\x83\x8b\xbd\x75\x11\xe3\x7c\x26\xf5\x38\xb5\xd1\xfe\x8f\x1a\x66\x17\xf7\x36\x62\x77\xe2\x4c\x59\x60\xc3\x2a\xda\x08\x0d\x4f\x09\x40\x7a\x9d\xa5\xdf\x71\xb9\x61\x3b\xc0\xfe\xdb\xdc\x36\x81\x49\xea\x57\xf2\xc6\xdd\x92\xfe\xb2\xfc\x01\xc9\xe6\x7a\xe7\x7b\x3e\xf9\xbb\x64\x4e\xc8\x4f\x00\x00\x00\xff\xff\xcc\xa0\x4d\x54\xa3\x02\x00\x00") + +func _1593601728_initial_schemaUpSqlBytes() ([]byte, error) { + return bindataRead( + __1593601728_initial_schemaUpSql, + "1593601728_initial_schema.up.sql", + ) +} + +func _1593601728_initial_schemaUpSql() (*asset, error) { + bytes, err := _1593601728_initial_schemaUpSqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "1593601728_initial_schema.up.sql", size: 675, mode: os.FileMode(0644), modTime: time.Unix(1595832279, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xfd, 0x61, 0x90, 0x79, 0xd9, 0x14, 0x65, 0xe9, 0x96, 0x53, 0x17, 0x33, 0x54, 0xeb, 0x8b, 0x5d, 0x95, 0x99, 0x10, 0x36, 0x58, 0xdd, 0xb2, 0xbf, 0x45, 0xd9, 0xbb, 0xc4, 0x92, 0xe, 0xce, 0x2}} + return a, nil +} + +var _docGo = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x8f\x3d\x6e\xec\x30\x0c\x84\x7b\x9d\x62\xb0\xcd\x36\xcf\x52\xf3\xaa\x74\x29\xd3\xe7\x02\x5c\x89\x96\x88\xb5\x24\x43\xa4\xf7\xe7\xf6\x81\x37\x01\xe2\x2e\xed\x87\xf9\x86\xc3\x10\xf0\x59\x44\x31\xcb\xc2\x10\x45\xe3\xc8\xaa\x34\x9e\xb8\x70\xa4\x4d\x19\xa7\x2c\x56\xb6\x8b\x8f\xbd\x06\x35\xb2\x4d\x27\xa9\xa1\x4a\x1e\x64\x1c\x6e\xff\x4f\x2e\x04\x44\x6a\x67\x43\xa1\x96\x16\x7e\x75\x29\xd4\x68\x98\xb4\x8c\xbb\x58\x01\x61\x1d\x3c\xcb\xc3\xe3\xdd\xb0\x30\xa9\xc1\x0a\xd9\x59\x61\x85\x11\x49\x79\xaf\x99\xfb\x40\xee\xd3\x45\x5a\x22\x23\xbf\xa3\x8f\xf9\x40\xf6\x85\x91\x96\x85\x13\xe6\xd1\xeb\xcb\x55\xaa\x8c\x24\x83\xa3\xf5\xf1\xfc\x07\x52\x65\x43\xa3\xca\xba\xfb\x85\x6e\x8c\xd6\x7f\xce\x83\x5a\xfa\xfb\x23\xdc\xfb\xb8\x2a\x48\xc1\x8f\x95\xa3\x71\xf2\xce\xad\x14\xaf\x94\x19\xdf\x39\xe9\x4d\x9d\x0b\x21\xf7\xb7\xcc\x8d\x77\xf3\xb8\x73\x5a\xaf\xf9\x90\xc4\xd4\xe1\x7d\xf8\x05\x3e\x77\xf8\xe0\xbe\x02\x00\x00\xff\xff\x4d\x1d\x5d\x50\x7e\x01\x00\x00") + +func docGoBytes() ([]byte, error) { + return bindataRead( + _docGo, + "doc.go", + ) +} + +func docGo() (*asset, error) { + bytes, err := docGoBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "doc.go", size: 382, mode: os.FileMode(0644), modTime: time.Unix(1595832279, 0)} + a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xc0, 0x2f, 0x1e, 0x64, 0x9, 0x93, 0xe4, 0x8b, 0xf2, 0x98, 0x5a, 0x45, 0xe2, 0x80, 0x88, 0x67, 0x7a, 0x2d, 0xd7, 0x4b, 0xd1, 0x73, 0xb6, 0x6d, 0x15, 0xc2, 0x0, 0x34, 0xcd, 0xa0, 0xdb, 0x20}} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + canonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[canonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// AssetString returns the asset contents as a string (instead of a []byte). +func AssetString(name string) (string, error) { + data, err := Asset(name) + return string(data), err +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// MustAssetString is like AssetString but panics when Asset would return an +// error. It simplifies safe initialization of global variables. +func MustAssetString(name string) string { + return string(MustAsset(name)) +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + canonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[canonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetDigest returns the digest of the file with the given name. It returns an +// error if the asset could not be found or the digest could not be loaded. +func AssetDigest(name string) ([sha256.Size]byte, error) { + canonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[canonicalName]; ok { + a, err := f() + if err != nil { + return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err) + } + return a.digest, nil + } + return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name) +} + +// Digests returns a map of all known files and their checksums. +func Digests() (map[string][sha256.Size]byte, error) { + mp := make(map[string][sha256.Size]byte, len(_bindata)) + for name := range _bindata { + a, err := _bindata[name]() + if err != nil { + return nil, err + } + mp[name] = a.digest + } + return mp, nil +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "1593601728_initial_schema.down.sql": _1593601728_initial_schemaDownSql, + + "1593601728_initial_schema.up.sql": _1593601728_initial_schemaUpSql, + + "doc.go": docGo, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"}, +// AssetDir("data/img") would return []string{"a.png", "b.png"}, +// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + canonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(canonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} + +var _bintree = &bintree{nil, map[string]*bintree{ + "1593601728_initial_schema.down.sql": &bintree{_1593601728_initial_schemaDownSql, map[string]*bintree{}}, + "1593601728_initial_schema.up.sql": &bintree{_1593601728_initial_schemaUpSql, map[string]*bintree{}}, + "doc.go": &bintree{docGo, map[string]*bintree{}}, +}} + +// RestoreAsset restores an asset under the given directory. +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) +} + +// RestoreAssets restores an asset under the given directory recursively. +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + canonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) +} diff --git a/protocol/pushnotificationserver/migrations/sql/1593601728_initial_schema.down.sql b/protocol/pushnotificationserver/migrations/sql/1593601728_initial_schema.down.sql new file mode 100644 index 000000000..dc09dc636 --- /dev/null +++ b/protocol/pushnotificationserver/migrations/sql/1593601728_initial_schema.down.sql @@ -0,0 +1,4 @@ +DROP TABLE push_notification_server_registrations; +DROP INDEX idx_push_notification_server_registrations_public_key; +DROP INDEX idx_push_notification_server_registrations_public_key_installation_id; + diff --git a/protocol/pushnotificationserver/migrations/sql/1593601728_initial_schema.up.sql b/protocol/pushnotificationserver/migrations/sql/1593601728_initial_schema.up.sql new file mode 100644 index 000000000..d5020d7b2 --- /dev/null +++ b/protocol/pushnotificationserver/migrations/sql/1593601728_initial_schema.up.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS push_notification_server_registrations ( + public_key BLOB NOT NULL, + installation_id VARCHAR NOT NULL, + version INT NOT NULL, + registration BLOB, + UNIQUE(public_key, installation_id) ON CONFLICT REPLACE +); + +CREATE TABLE IF NOT EXISTS push_notification_server_identity ( + private_key BLOB NOT NULL, + synthetic_id INT NOT NULL DEFAULT 0, + UNIQUE(synthetic_id) +); + +CREATE INDEX idx_push_notification_server_registrations_public_key ON push_notification_server_registrations(public_key); +CREATE INDEX idx_push_notification_server_registrations_public_key_installation_id ON push_notification_server_registrations(public_key, installation_id); + diff --git a/protocol/pushnotificationserver/migrations/sql/doc.go b/protocol/pushnotificationserver/migrations/sql/doc.go new file mode 100644 index 000000000..a7d080561 --- /dev/null +++ b/protocol/pushnotificationserver/migrations/sql/doc.go @@ -0,0 +1,9 @@ +// This file is necessary because "github.com/status-im/migrate/v4" +// can't handle files starting with a prefix. At least that's the case +// for go-bindata. +// If go-bindata is called from the same directory, asset names +// have no prefix and "github.com/status-im/migrate/v4" works as expected. + +package migrations + +//go:generate go-bindata -pkg migrations -o ../migrations.go ./ diff --git a/protocol/pushnotificationserver/persistence.go b/protocol/pushnotificationserver/persistence.go new file mode 100644 index 000000000..0f31fb178 --- /dev/null +++ b/protocol/pushnotificationserver/persistence.go @@ -0,0 +1,154 @@ +package pushnotificationserver + +import ( + "crypto/ecdsa" + "database/sql" + "strings" + + "github.com/golang/protobuf/proto" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/protocol/protobuf" +) + +type Persistence interface { + // GetPushNotificationRegistrationByPublicKeyAndInstallationID retrieve a push notification registration from storage given a public key and installation id + GetPushNotificationRegistrationByPublicKeyAndInstallationID(publicKey []byte, installationID string) (*protobuf.PushNotificationRegistration, error) + // GetPushNotificationRegistrationByPublicKey retrieve all the push notification registrations from storage given a public key + GetPushNotificationRegistrationByPublicKeys(publicKeys [][]byte) ([]*PushNotificationIDAndRegistration, error) + //GetPushNotificationRegistrationPublicKeys return all the public keys stored + GetPushNotificationRegistrationPublicKeys() ([][]byte, error) + + // DeletePushNotificationRegistration deletes a push notification registration from storage given a public key and installation id + DeletePushNotificationRegistration(publicKey []byte, installationID string) error + // SavePushNotificationRegistration saves a push notification option to the db + SavePushNotificationRegistration(publicKey []byte, registration *protobuf.PushNotificationRegistration) error + // GetIdentity returns the server identity key + GetIdentity() (*ecdsa.PrivateKey, error) + // SaveIdentity saves the server identity key + SaveIdentity(*ecdsa.PrivateKey) error +} + +type SQLitePersistence struct { + db *sql.DB +} + +func NewSQLitePersistence(db *sql.DB) Persistence { + return &SQLitePersistence{db: db} +} + +func (p *SQLitePersistence) GetPushNotificationRegistrationByPublicKeyAndInstallationID(publicKey []byte, installationID string) (*protobuf.PushNotificationRegistration, error) { + var marshaledRegistration []byte + err := p.db.QueryRow(`SELECT registration FROM push_notification_server_registrations WHERE public_key = ? AND installation_id = ?`, publicKey, installationID).Scan(&marshaledRegistration) + + if err == sql.ErrNoRows { + return nil, nil + } else if err != nil { + return nil, err + } + + registration := &protobuf.PushNotificationRegistration{} + + if err := proto.Unmarshal(marshaledRegistration, registration); err != nil { + return nil, err + } + return registration, nil +} + +type PushNotificationIDAndRegistration struct { + ID []byte + Registration *protobuf.PushNotificationRegistration +} + +func (p *SQLitePersistence) GetPushNotificationRegistrationByPublicKeys(publicKeys [][]byte) ([]*PushNotificationIDAndRegistration, error) { + // TODO: check for a max number of keys + + publicKeyArgs := make([]interface{}, 0, len(publicKeys)) + for _, pk := range publicKeys { + publicKeyArgs = append(publicKeyArgs, pk) + } + + inVector := strings.Repeat("?, ", len(publicKeys)-1) + "?" + + rows, err := p.db.Query(`SELECT public_key,registration FROM push_notification_server_registrations WHERE public_key IN (`+inVector+`)`, publicKeyArgs...) // nolint: gosec + if err != nil { + return nil, err + } + defer rows.Close() + + var registrations []*PushNotificationIDAndRegistration + for rows.Next() { + response := &PushNotificationIDAndRegistration{} + var marshaledRegistration []byte + err := rows.Scan(&response.ID, &marshaledRegistration) + if err != nil { + return nil, err + } + + registration := &protobuf.PushNotificationRegistration{} + + if err := proto.Unmarshal(marshaledRegistration, registration); err != nil { + return nil, err + } + response.Registration = registration + registrations = append(registrations, response) + } + return registrations, nil +} + +func (p *SQLitePersistence) GetPushNotificationRegistrationPublicKeys() ([][]byte, error) { + rows, err := p.db.Query(`SELECT public_key FROM push_notification_server_registrations`) + if err != nil { + return nil, err + } + defer rows.Close() + + var publicKeys [][]byte + for rows.Next() { + var publicKey []byte + err := rows.Scan(&publicKey) + if err != nil { + return nil, err + } + + publicKeys = append(publicKeys, publicKey) + } + return publicKeys, nil +} + +func (p *SQLitePersistence) SavePushNotificationRegistration(publicKey []byte, registration *protobuf.PushNotificationRegistration) error { + marshaledRegistration, err := proto.Marshal(registration) + if err != nil { + return err + } + + _, err = p.db.Exec(`INSERT INTO push_notification_server_registrations (public_key, installation_id, version, registration) VALUES (?, ?, ?, ?)`, publicKey, registration.InstallationId, registration.Version, marshaledRegistration) + return err +} + +func (p *SQLitePersistence) DeletePushNotificationRegistration(publicKey []byte, installationID string) error { + _, err := p.db.Exec(`DELETE FROM push_notification_server_registrations WHERE public_key = ? AND installation_id = ?`, publicKey, installationID) + return err +} + +func (p *SQLitePersistence) SaveIdentity(privateKey *ecdsa.PrivateKey) error { + _, err := p.db.Exec(`INSERT INTO push_notification_server_identity (private_key) VALUES (?)`, crypto.FromECDSA(privateKey)) + return err +} + +func (p *SQLitePersistence) GetIdentity() (*ecdsa.PrivateKey, error) { + var pkBytes []byte + err := p.db.QueryRow(`SELECT private_key FROM push_notification_server_identity LIMIT 1`).Scan(&pkBytes) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, err + } + + pk, err := crypto.ToECDSA(pkBytes) + if err != nil { + return nil, err + } + return pk, nil +} diff --git a/protocol/pushnotificationserver/persistence_test.go b/protocol/pushnotificationserver/persistence_test.go new file mode 100644 index 000000000..31029f99d --- /dev/null +++ b/protocol/pushnotificationserver/persistence_test.go @@ -0,0 +1,83 @@ +package pushnotificationserver + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/suite" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/sqlite" +) + +func TestSQLitePersistenceSuite(t *testing.T) { + suite.Run(t, new(SQLitePersistenceSuite)) +} + +type SQLitePersistenceSuite struct { + suite.Suite + tmpFile *os.File + persistence Persistence +} + +func (s *SQLitePersistenceSuite) SetupTest() { + tmpFile, err := ioutil.TempFile("", "") + s.Require().NoError(err) + s.tmpFile = tmpFile + + database, err := sqlite.Open(s.tmpFile.Name(), "") + s.Require().NoError(err) + s.persistence = NewSQLitePersistence(database) +} + +func (s *SQLitePersistenceSuite) TearDownTest() { + _ = os.Remove(s.tmpFile.Name()) +} + +func (s *SQLitePersistenceSuite) TestSaveAndRetrieve() { + key, err := crypto.GenerateKey() + s.Require().NoError(err) + installationID := "54242d02-bb92-11ea-b3de-0242ac130004" + + registration := &protobuf.PushNotificationRegistration{ + InstallationId: installationID, + Version: 5, + } + + s.Require().NoError(s.persistence.SavePushNotificationRegistration(common.HashPublicKey(&key.PublicKey), registration)) + + retrievedRegistration, err := s.persistence.GetPushNotificationRegistrationByPublicKeyAndInstallationID(common.HashPublicKey(&key.PublicKey), installationID) + s.Require().NoError(err) + + s.Require().True(proto.Equal(registration, retrievedRegistration)) +} + +func (s *SQLitePersistenceSuite) TestSaveAndRetrieveIdentity() { + retrievedKey, err := s.persistence.GetIdentity() + s.Require().NoError(err) + s.Require().Nil(retrievedKey) + + key, err := crypto.GenerateKey() + s.Require().NoError(err) + s.Require().NoError(s.persistence.SaveIdentity(key)) + + retrievedKey, err = s.persistence.GetIdentity() + s.Require().NoError(err) + + s.Require().Equal(key, retrievedKey) +} + +func (s *SQLitePersistenceSuite) TestSaveDifferentIdenities() { + key1, err := crypto.GenerateKey() + s.Require().NoError(err) + key2, err := crypto.GenerateKey() + s.Require().NoError(err) + + // First one should be successul, second should fail + s.Require().NoError(s.persistence.SaveIdentity(key1)) + s.Require().Error(s.persistence.SaveIdentity(key2)) +} diff --git a/protocol/pushnotificationserver/server.go b/protocol/pushnotificationserver/server.go new file mode 100644 index 000000000..03bacca00 --- /dev/null +++ b/protocol/pushnotificationserver/server.go @@ -0,0 +1,434 @@ +package pushnotificationserver + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "errors" + + "github.com/golang/protobuf/proto" + "github.com/google/uuid" + "go.uber.org/zap" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/crypto/ecies" + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/protobuf" +) + +const encryptedPayloadKeyLength = 16 +const defaultGorushURL = "https://gorush.status.im" + +type Config struct { + // Identity is our identity key + Identity *ecdsa.PrivateKey + // GorushUrl is the url for the gorush service + GorushURL string + + Logger *zap.Logger +} + +type Server struct { + persistence Persistence + config *Config + messageProcessor *common.MessageProcessor +} + +func New(config *Config, persistence Persistence, messageProcessor *common.MessageProcessor) *Server { + if len(config.GorushURL) == 0 { + config.GorushURL = defaultGorushURL + + } + return &Server{persistence: persistence, config: config, messageProcessor: messageProcessor} +} + +func (s *Server) Start() error { + s.config.Logger.Info("starting push notification server") + if s.config.Identity == nil { + s.config.Logger.Info("Identity nil") + // Pull identity from database + identity, err := s.persistence.GetIdentity() + if err != nil { + return err + } + if identity == nil { + identity, err = crypto.GenerateKey() + if err != nil { + return err + } + if err := s.persistence.SaveIdentity(identity); err != nil { + return err + } + } + s.config.Identity = identity + } + + pks, err := s.persistence.GetPushNotificationRegistrationPublicKeys() + if err != nil { + return err + } + // listen to all topics for users registered + for _, pk := range pks { + if err := s.listenToPublicKeyQueryTopic(pk); err != nil { + return err + } + } + + s.config.Logger.Info("started push notification server", zap.String("identity", types.EncodeHex(crypto.FromECDSAPub(&s.config.Identity.PublicKey)))) + + return nil +} + +// HandlePushNotificationRegistration builds a response for the registration and sends it back to the user +func (s *Server) HandlePushNotificationRegistration(publicKey *ecdsa.PublicKey, payload []byte) error { + response := s.buildPushNotificationRegistrationResponse(publicKey, payload) + if response == nil { + return nil + } + encodedMessage, err := proto.Marshal(response) + if err != nil { + return err + } + + rawMessage := &common.RawMessage{ + Payload: encodedMessage, + MessageType: protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION_RESPONSE, + // we skip encryption as might be sent from an ephemeral key + SkipEncryption: true, + } + + _, err = s.messageProcessor.SendPrivate(context.Background(), publicKey, rawMessage) + return err +} + +// HandlePushNotificationQuery builds a response for the query and sends it back to the user +func (s *Server) HandlePushNotificationQuery(publicKey *ecdsa.PublicKey, messageID []byte, query protobuf.PushNotificationQuery) error { + response := s.buildPushNotificationQueryResponse(&query) + if response == nil { + return nil + } + response.MessageId = messageID + encodedMessage, err := proto.Marshal(response) + if err != nil { + return err + } + + rawMessage := &common.RawMessage{ + Payload: encodedMessage, + MessageType: protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_QUERY_RESPONSE, + // we skip encryption as sent from an ephemeral key + SkipEncryption: true, + } + + _, err = s.messageProcessor.SendPrivate(context.Background(), publicKey, rawMessage) + return err +} + +// HandlePushNotificationRequest will send a gorush notification and send a response back to the user +func (s *Server) HandlePushNotificationRequest(publicKey *ecdsa.PublicKey, + request protobuf.PushNotificationRequest) error { + s.config.Logger.Debug("handling pn request") + response := s.buildPushNotificationRequestResponseAndSendNotification(&request) + if response == nil { + return nil + } + encodedMessage, err := proto.Marshal(response) + if err != nil { + return err + } + + rawMessage := &common.RawMessage{ + Payload: encodedMessage, + MessageType: protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_RESPONSE, + // We skip encryption here as the message has been sent from an ephemeral key + SkipEncryption: true, + } + + _, err = s.messageProcessor.SendPrivate(context.Background(), publicKey, rawMessage) + return err +} + +// buildGrantSignatureMaterial builds a grant for a specific server. +// We use 3 components: +// 1) The client public key. Not sure this applies to our signature scheme, but best to be conservative. https://crypto.stackexchange.com/questions/15538/given-a-message-and-signature-find-a-public-key-that-makes-the-signature-valid +// 2) The server public key +// 3) The access token +// By verifying this signature, a client can trust the server was instructed to store this access token. +func (s *Server) buildGrantSignatureMaterial(clientPublicKey *ecdsa.PublicKey, serverPublicKey *ecdsa.PublicKey, accessToken string) []byte { + var signatureMaterial []byte + signatureMaterial = append(signatureMaterial, crypto.CompressPubkey(clientPublicKey)...) + signatureMaterial = append(signatureMaterial, crypto.CompressPubkey(serverPublicKey)...) + signatureMaterial = append(signatureMaterial, []byte(accessToken)...) + a := crypto.Keccak256(signatureMaterial) + return a +} + +func (s *Server) verifyGrantSignature(clientPublicKey *ecdsa.PublicKey, accessToken string, grant []byte) error { + signatureMaterial := s.buildGrantSignatureMaterial(clientPublicKey, &s.config.Identity.PublicKey, accessToken) + recoveredPublicKey, err := crypto.SigToPub(signatureMaterial, grant) + if err != nil { + return err + } + + if !common.IsPubKeyEqual(recoveredPublicKey, clientPublicKey) { + return errors.New("pubkey mismatch") + } + return nil + +} + +func (s *Server) generateSharedKey(publicKey *ecdsa.PublicKey) ([]byte, error) { + return ecies.ImportECDSA(s.config.Identity).GenerateShared( + ecies.ImportECDSAPublic(publicKey), + encryptedPayloadKeyLength, + encryptedPayloadKeyLength, + ) +} + +func (s *Server) validateUUID(u string) error { + if len(u) == 0 { + return errors.New("empty uuid") + } + _, err := uuid.Parse(u) + return err +} + +func (s *Server) decryptRegistration(publicKey *ecdsa.PublicKey, payload []byte) ([]byte, error) { + sharedKey, err := s.generateSharedKey(publicKey) + if err != nil { + return nil, err + } + + return common.Decrypt(payload, sharedKey) +} + +// validateRegistration validates a new message against the last one received for a given installationID and and public key +// and return the decrypted message +func (s *Server) validateRegistration(publicKey *ecdsa.PublicKey, payload []byte) (*protobuf.PushNotificationRegistration, error) { + if payload == nil { + return nil, ErrEmptyPushNotificationRegistrationPayload + } + + if publicKey == nil { + return nil, ErrEmptyPushNotificationRegistrationPublicKey + } + + decryptedPayload, err := s.decryptRegistration(publicKey, payload) + if err != nil { + return nil, err + } + + registration := &protobuf.PushNotificationRegistration{} + + if err := proto.Unmarshal(decryptedPayload, registration); err != nil { + return nil, ErrCouldNotUnmarshalPushNotificationRegistration + } + + if registration.Version < 1 { + return nil, ErrInvalidPushNotificationRegistrationVersion + } + + if err := s.validateUUID(registration.InstallationId); err != nil { + return nil, ErrMalformedPushNotificationRegistrationInstallationID + } + + previousRegistration, err := s.persistence.GetPushNotificationRegistrationByPublicKeyAndInstallationID(common.HashPublicKey(publicKey), registration.InstallationId) + if err != nil { + return nil, err + } + + if previousRegistration != nil && registration.Version <= previousRegistration.Version { + return nil, ErrInvalidPushNotificationRegistrationVersion + } + + // unregistering message + if registration.Unregister { + return registration, nil + } + + if err := s.validateUUID(registration.AccessToken); err != nil { + return nil, ErrMalformedPushNotificationRegistrationAccessToken + } + + if len(registration.Grant) == 0 { + return nil, ErrMalformedPushNotificationRegistrationGrant + } + + if err := s.verifyGrantSignature(publicKey, registration.AccessToken, registration.Grant); err != nil { + + s.config.Logger.Error("failed to verify grant", zap.Error(err)) + return nil, ErrMalformedPushNotificationRegistrationGrant + } + + if len(registration.DeviceToken) == 0 { + return nil, ErrMalformedPushNotificationRegistrationDeviceToken + } + + if registration.TokenType == protobuf.PushNotificationRegistration_UNKNOWN_TOKEN_TYPE { + return nil, ErrUnknownPushNotificationRegistrationTokenType + } + + return registration, nil +} + +// buildPushNotificationQueryResponse check if we have the client information and send them back +func (s *Server) buildPushNotificationQueryResponse(query *protobuf.PushNotificationQuery) *protobuf.PushNotificationQueryResponse { + + s.config.Logger.Debug("handling push notification query") + response := &protobuf.PushNotificationQueryResponse{} + if query == nil || len(query.PublicKeys) == 0 { + return response + } + + registrations, err := s.persistence.GetPushNotificationRegistrationByPublicKeys(query.PublicKeys) + if err != nil { + s.config.Logger.Error("failed to retrieve registration", zap.Error(err)) + return response + } + + for _, idAndResponse := range registrations { + + registration := idAndResponse.Registration + info := &protobuf.PushNotificationQueryInfo{ + PublicKey: idAndResponse.ID, + Grant: registration.Grant, + Version: registration.Version, + InstallationId: registration.InstallationId, + } + + // if instructed to only allow from contacts, send back a list + if registration.AllowFromContactsOnly { + info.AllowedKeyList = registration.AllowedKeyList + } else { + info.AccessToken = registration.AccessToken + } + response.Info = append(response.Info, info) + } + + response.Success = true + return response +} + +// buildPushNotificationRequestResponseAndSendNotification will build a response +// and fire-and-forget send a query to the gorush instance +func (s *Server) buildPushNotificationRequestResponseAndSendNotification(request *protobuf.PushNotificationRequest) *protobuf.PushNotificationResponse { + response := &protobuf.PushNotificationResponse{} + // We don't even send a response in this case + if request == nil || len(request.MessageId) == 0 { + s.config.Logger.Warn("empty message id") + return nil + } + + response.MessageId = request.MessageId + + // TODO: filter by chat id + // collect successful requests & registrations + var requestAndRegistrations []*RequestAndRegistration + + for _, pn := range request.Requests { + registration, err := s.persistence.GetPushNotificationRegistrationByPublicKeyAndInstallationID(pn.PublicKey, pn.InstallationId) + report := &protobuf.PushNotificationReport{ + PublicKey: pn.PublicKey, + InstallationId: pn.InstallationId, + } + + if err != nil { + s.config.Logger.Error("failed to retrieve registration", zap.Error(err)) + report.Error = protobuf.PushNotificationReport_UNKNOWN_ERROR_TYPE + } else if registration == nil { + s.config.Logger.Warn("empty registration") + report.Error = protobuf.PushNotificationReport_NOT_REGISTERED + } else if registration.AccessToken != pn.AccessToken { + s.config.Logger.Warn("invalid access token") + report.Error = protobuf.PushNotificationReport_WRONG_TOKEN + } else { + // For now we just assume that the notification will be successful + requestAndRegistrations = append(requestAndRegistrations, &RequestAndRegistration{ + Request: pn, + Registration: registration, + }) + report.Success = true + } + + response.Reports = append(response.Reports, report) + } + + s.config.Logger.Debug("built pn request") + if len(requestAndRegistrations) == 0 { + s.config.Logger.Warn("no request and registration") + return response + } + + // This can be done asynchronously + goRushRequest := PushNotificationRegistrationToGoRushRequest(requestAndRegistrations) + err := sendGoRushNotification(goRushRequest, s.config.GorushURL) + if err != nil { + s.config.Logger.Error("failed to send go rush notification", zap.Error(err)) + // TODO: handle this error? + // GoRush will not let us know that the sending of the push notification has failed, + // so this likely mean that the actual HTTP request has failed, or there was some unexpected error + } + + return response +} + +// listenToPublicKeyQueryTopic listen to a topic derived from the hashed public key +func (s *Server) listenToPublicKeyQueryTopic(hashedPublicKey []byte) error { + if s.messageProcessor == nil { + return nil + } + encodedPublicKey := hex.EncodeToString(hashedPublicKey) + return s.messageProcessor.JoinPublic(encodedPublicKey) +} + +// buildPushNotificationRegistrationResponse will check the registration is valid, save it, and listen to the topic for the queries +func (s *Server) buildPushNotificationRegistrationResponse(publicKey *ecdsa.PublicKey, payload []byte) *protobuf.PushNotificationRegistrationResponse { + + s.config.Logger.Info("handling push notification registration") + response := &protobuf.PushNotificationRegistrationResponse{ + RequestId: common.Shake256(payload), + } + + registration, err := s.validateRegistration(publicKey, payload) + + if err != nil { + if err == ErrInvalidPushNotificationRegistrationVersion { + response.Error = protobuf.PushNotificationRegistrationResponse_VERSION_MISMATCH + } else { + response.Error = protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE + } + s.config.Logger.Warn("registration did not validate", zap.Error(err)) + return response + } + + if registration.Unregister { + // We save an empty registration, only keeping version and installation-id + emptyRegistration := &protobuf.PushNotificationRegistration{ + Version: registration.Version, + InstallationId: registration.InstallationId, + } + if err := s.persistence.SavePushNotificationRegistration(common.HashPublicKey(publicKey), emptyRegistration); err != nil { + response.Error = protobuf.PushNotificationRegistrationResponse_INTERNAL_ERROR + s.config.Logger.Error("failed to unregister ", zap.Error(err)) + return response + } + + } else if err := s.persistence.SavePushNotificationRegistration(common.HashPublicKey(publicKey), registration); err != nil { + response.Error = protobuf.PushNotificationRegistrationResponse_INTERNAL_ERROR + s.config.Logger.Error("failed to save registration", zap.Error(err)) + return response + } + + if err := s.listenToPublicKeyQueryTopic(common.HashPublicKey(publicKey)); err != nil { + response.Error = protobuf.PushNotificationRegistrationResponse_INTERNAL_ERROR + s.config.Logger.Error("failed to listen to topic", zap.Error(err)) + return response + + } + response.Success = true + + s.config.Logger.Info("handled push notification registration successfully") + + return response +} diff --git a/protocol/pushnotificationserver/server_test.go b/protocol/pushnotificationserver/server_test.go new file mode 100644 index 000000000..77613ce50 --- /dev/null +++ b/protocol/pushnotificationserver/server_test.go @@ -0,0 +1,584 @@ +package pushnotificationserver + +import ( + "crypto/ecdsa" + "crypto/rand" + "io/ioutil" + "os" + "testing" + + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/suite" + + "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/protobuf" + "github.com/status-im/status-go/protocol/sqlite" + "github.com/status-im/status-go/protocol/tt" +) + +func TestServerSuite(t *testing.T) { + s := new(ServerSuite) + s.accessToken = "b6ae4fde-bb65-11ea-b3de-0242ac130004" + s.installationID = "c6ae4fde-bb65-11ea-b3de-0242ac130004" + + suite.Run(t, s) +} + +type ServerSuite struct { + suite.Suite + tmpFile *os.File + persistence Persistence + accessToken string + installationID string + identity *ecdsa.PrivateKey + key *ecdsa.PrivateKey + sharedKey []byte + grant []byte + server *Server +} + +func (s *ServerSuite) SetupTest() { + tmpFile, err := ioutil.TempFile("", "") + s.Require().NoError(err) + s.tmpFile = tmpFile + + database, err := sqlite.Open(s.tmpFile.Name(), "") + s.Require().NoError(err) + s.persistence = NewSQLitePersistence(database) + + identity, err := crypto.GenerateKey() + s.Require().NoError(err) + s.identity = identity + + key, err := crypto.GenerateKey() + s.Require().NoError(err) + s.key = key + + config := &Config{ + Identity: identity, + Logger: tt.MustCreateTestLogger(), + } + + s.server = New(config, s.persistence, nil) + + sharedKey, err := s.server.generateSharedKey(&s.key.PublicKey) + s.Require().NoError(err) + s.sharedKey = sharedKey + signatureMaterial := s.server.buildGrantSignatureMaterial(&s.key.PublicKey, &identity.PublicKey, s.accessToken) + grant, err := crypto.Sign(signatureMaterial, s.key) + s.Require().NoError(err) + + s.grant = grant + +} + +func (s *ServerSuite) TestPushNotificationServerValidateRegistration() { + + // Empty payload + _, err := s.server.validateRegistration(&s.key.PublicKey, nil) + s.Require().Equal(ErrEmptyPushNotificationRegistrationPayload, err) + + // Empty key + _, err = s.server.validateRegistration(nil, []byte("payload")) + s.Require().Equal(ErrEmptyPushNotificationRegistrationPublicKey, err) + + // Invalid cyphertext length + _, err = s.server.validateRegistration(&s.key.PublicKey, []byte("too short")) + s.Require().Equal(common.ErrInvalidCiphertextLength, err) + + // Invalid cyphertext length + _, err = s.server.validateRegistration(&s.key.PublicKey, []byte("too short")) + s.Require().Equal(common.ErrInvalidCiphertextLength, err) + + // Invalid ciphertext + _, err = s.server.validateRegistration(&s.key.PublicKey, []byte("not too short but invalid")) + s.Require().Error(common.ErrInvalidCiphertextLength, err) + + // Different key ciphertext + cyphertext, err := common.Encrypt([]byte("plaintext"), make([]byte, 32), rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Error(err) + + // Right cyphertext but non unmarshable payload + cyphertext, err = common.Encrypt([]byte("plaintext"), s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrCouldNotUnmarshalPushNotificationRegistration, err) + + // Missing installationID + payload, err := proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + Grant: s.grant, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrMalformedPushNotificationRegistrationInstallationID, err) + + // Malformed installationID + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + Grant: s.grant, + InstallationId: "abc", + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrMalformedPushNotificationRegistrationInstallationID, err) + + // Version set to 0 + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + Grant: s.grant, + InstallationId: s.installationID, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrInvalidPushNotificationRegistrationVersion, err) + + // Version lower than previous one + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + Grant: s.grant, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + InstallationId: s.installationID, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + + // Setup persistence + s.Require().NoError(s.persistence.SavePushNotificationRegistration(common.HashPublicKey(&s.key.PublicKey), &protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + Grant: s.grant, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + InstallationId: s.installationID, + Version: 2})) + + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrInvalidPushNotificationRegistrationVersion, err) + + // Cleanup persistence + s.Require().NoError(s.persistence.DeletePushNotificationRegistration(common.HashPublicKey(&s.key.PublicKey), s.installationID)) + + // Unregistering message + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + InstallationId: s.installationID, + Unregister: true, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Nil(err) + + // Missing access token + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + InstallationId: s.installationID, + Grant: s.grant, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrMalformedPushNotificationRegistrationAccessToken, err) + + // Invalid access token + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: "bc", + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + Grant: s.grant, + InstallationId: s.installationID, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrMalformedPushNotificationRegistrationAccessToken, err) + + // Missing device token + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + Grant: s.grant, + InstallationId: s.installationID, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrMalformedPushNotificationRegistrationDeviceToken, err) + + // Missing grant + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + DeviceToken: "device-token", + InstallationId: s.installationID, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrMalformedPushNotificationRegistrationGrant, err) + + // Invalid grant + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + DeviceToken: "device-token", + Grant: crypto.Keccak256([]byte("invalid")), + InstallationId: s.installationID, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrMalformedPushNotificationRegistrationGrant, err) + + // Missing token type + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + DeviceToken: "device-token", + Grant: s.grant, + InstallationId: s.installationID, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().Equal(ErrUnknownPushNotificationRegistrationTokenType, err) + + // Successful + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + DeviceToken: "abc", + AccessToken: s.accessToken, + Grant: s.grant, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + InstallationId: s.installationID, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + _, err = s.server.validateRegistration(&s.key.PublicKey, cyphertext) + s.Require().NoError(err) +} + +func (s *ServerSuite) TestPushNotificationHandleRegistration() { + // Empty payload + response := s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, nil) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Empty key + response = s.server.buildPushNotificationRegistrationResponse(nil, []byte("payload")) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Invalid cyphertext length + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, []byte("too short")) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Invalid cyphertext length + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, []byte("too short")) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Invalid ciphertext + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, []byte("not too short but invalid")) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Different key ciphertext + cyphertext, err := common.Encrypt([]byte("plaintext"), make([]byte, 32), rand.Reader) + s.Require().NoError(err) + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Right cyphertext but non unmarshable payload + cyphertext, err = common.Encrypt([]byte("plaintext"), s.sharedKey, rand.Reader) + s.Require().NoError(err) + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Missing installationID + payload, err := proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + Grant: s.grant, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Malformed installationID + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + InstallationId: "abc", + Grant: s.grant, + Version: 1, + }) + s.Require().NoError(err) + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Version set to 0 + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + Grant: s.grant, + InstallationId: s.installationID, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_VERSION_MISMATCH) + + // Version lower than previous one + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + Grant: s.grant, + InstallationId: s.installationID, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + + // Setup persistence + s.Require().NoError(s.persistence.SavePushNotificationRegistration(common.HashPublicKey(&s.key.PublicKey), &protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + Grant: s.grant, + InstallationId: s.installationID, + Version: 2})) + + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_VERSION_MISMATCH) + + // Cleanup persistence + s.Require().NoError(s.persistence.DeletePushNotificationRegistration(common.HashPublicKey(&s.key.PublicKey), s.installationID)) + + // Missing access token + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + InstallationId: s.installationID, + Grant: s.grant, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Invalid access token + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: "bc", + Grant: s.grant, + InstallationId: s.installationID, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Missing device token + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + AccessToken: s.accessToken, + Grant: s.grant, + InstallationId: s.installationID, + Version: 1, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().False(response.Success) + s.Require().Equal(response.Error, protobuf.PushNotificationRegistrationResponse_MALFORMED_MESSAGE) + + // Successful + registration := &protobuf.PushNotificationRegistration{ + DeviceToken: "abc", + AccessToken: s.accessToken, + Grant: s.grant, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + InstallationId: s.installationID, + Version: 1, + } + payload, err = proto.Marshal(registration) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().True(response.Success) + + // Pull from the db + retrievedRegistration, err := s.persistence.GetPushNotificationRegistrationByPublicKeyAndInstallationID(common.HashPublicKey(&s.key.PublicKey), s.installationID) + s.Require().NoError(err) + s.Require().NotNil(retrievedRegistration) + s.Require().True(proto.Equal(retrievedRegistration, registration)) + + // Unregistering message + payload, err = proto.Marshal(&protobuf.PushNotificationRegistration{ + DeviceToken: "token", + InstallationId: s.installationID, + Unregister: true, + Version: 2, + }) + s.Require().NoError(err) + + cyphertext, err = common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + response = s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().True(response.Success) + + // Check is gone from the db + retrievedRegistration, err = s.persistence.GetPushNotificationRegistrationByPublicKeyAndInstallationID(common.HashPublicKey(&s.key.PublicKey), s.installationID) + s.Require().NoError(err) + s.Require().NotNil(retrievedRegistration) + s.Require().Empty(retrievedRegistration.AccessToken) + s.Require().Empty(retrievedRegistration.DeviceToken) + s.Require().Equal(uint64(2), retrievedRegistration.Version) + s.Require().Equal(s.installationID, retrievedRegistration.InstallationId) + s.Require().Equal(common.Shake256(cyphertext), response.RequestId) +} + +func (s *ServerSuite) TestbuildPushNotificationQueryResponseNoFiltering() { + hashedPublicKey := common.HashPublicKey(&s.key.PublicKey) + // Successful + registration := &protobuf.PushNotificationRegistration{ + DeviceToken: "abc", + AccessToken: s.accessToken, + Grant: s.grant, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + InstallationId: s.installationID, + Version: 1, + } + payload, err := proto.Marshal(registration) + s.Require().NoError(err) + + cyphertext, err := common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + response := s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().True(response.Success) + + query := &protobuf.PushNotificationQuery{ + PublicKeys: [][]byte{[]byte("non-existing"), hashedPublicKey}, + } + + queryResponse := s.server.buildPushNotificationQueryResponse(query) + s.Require().NotNil(queryResponse) + s.Require().True(queryResponse.Success) + s.Require().Len(queryResponse.Info, 1) + s.Require().Equal(s.accessToken, queryResponse.Info[0].AccessToken) + s.Require().Equal(hashedPublicKey, queryResponse.Info[0].PublicKey) + s.Require().Equal(s.installationID, queryResponse.Info[0].InstallationId) + s.Require().Nil(queryResponse.Info[0].AllowedKeyList) +} + +func (s *ServerSuite) TestbuildPushNotificationQueryResponseWithFiltering() { + hashedPublicKey := common.HashPublicKey(&s.key.PublicKey) + allowedKeyList := [][]byte{[]byte("a")} + // Successful + + registration := &protobuf.PushNotificationRegistration{ + DeviceToken: "abc", + AccessToken: s.accessToken, + Grant: s.grant, + TokenType: protobuf.PushNotificationRegistration_APN_TOKEN, + InstallationId: s.installationID, + AllowFromContactsOnly: true, + AllowedKeyList: allowedKeyList, + Version: 1, + } + payload, err := proto.Marshal(registration) + s.Require().NoError(err) + + cyphertext, err := common.Encrypt(payload, s.sharedKey, rand.Reader) + s.Require().NoError(err) + response := s.server.buildPushNotificationRegistrationResponse(&s.key.PublicKey, cyphertext) + s.Require().NotNil(response) + s.Require().True(response.Success) + + query := &protobuf.PushNotificationQuery{ + PublicKeys: [][]byte{[]byte("non-existing"), hashedPublicKey}, + } + + queryResponse := s.server.buildPushNotificationQueryResponse(query) + s.Require().NotNil(queryResponse) + s.Require().True(queryResponse.Success) + s.Require().Len(queryResponse.Info, 1) + s.Require().Equal(hashedPublicKey, queryResponse.Info[0].PublicKey) + s.Require().Equal(s.installationID, queryResponse.Info[0].InstallationId) + s.Require().Equal(allowedKeyList, queryResponse.Info[0].AllowedKeyList) +} diff --git a/protocol/sqlite/migrations.go b/protocol/sqlite/migrations.go index 387f7608d..5c9eb035d 100644 --- a/protocol/sqlite/migrations.go +++ b/protocol/sqlite/migrations.go @@ -7,6 +7,8 @@ import ( encryptmigrations "github.com/status-im/status-go/protocol/encryption/migrations" appmigrations "github.com/status-im/status-go/protocol/migrations" + push_notification_client_migrations "github.com/status-im/status-go/protocol/pushnotificationclient/migrations" + push_notification_server_migrations "github.com/status-im/status-go/protocol/pushnotificationserver/migrations" wakumigrations "github.com/status-im/status-go/protocol/transport/waku/migrations" whispermigrations "github.com/status-im/status-go/protocol/transport/whisper/migrations" ) @@ -35,6 +37,14 @@ var defaultMigrations = []migrationsWithGetter{ Names: appmigrations.AssetNames(), Getter: appmigrations.Asset, }, + { + Names: push_notification_server_migrations.AssetNames(), + Getter: push_notification_server_migrations.Asset, + }, + { + Names: push_notification_client_migrations.AssetNames(), + Getter: push_notification_client_migrations.Asset, + }, } func prepareMigrations(migrations []migrationsWithGetter) ([]string, getter, error) { diff --git a/protocol/transport/filters_manager.go b/protocol/transport/filters_manager.go index 7f49a8175..3e3da4b38 100644 --- a/protocol/transport/filters_manager.go +++ b/protocol/transport/filters_manager.go @@ -218,15 +218,15 @@ func (s *FiltersManager) Remove(filters ...*Filter) error { } // LoadPartitioned creates a filter for a partitioned topic. -func (s *FiltersManager) LoadPartitioned(publicKey *ecdsa.PublicKey) (*Filter, error) { - return s.loadPartitioned(publicKey, false) +func (s *FiltersManager) LoadPartitioned(publicKey *ecdsa.PublicKey, identity *ecdsa.PrivateKey, listen bool) (*Filter, error) { + return s.loadPartitioned(publicKey, identity, listen) } func (s *FiltersManager) loadMyPartitioned() (*Filter, error) { - return s.loadPartitioned(&s.privateKey.PublicKey, true) + return s.loadPartitioned(&s.privateKey.PublicKey, s.privateKey, true) } -func (s *FiltersManager) loadPartitioned(publicKey *ecdsa.PublicKey, listen bool) (*Filter, error) { +func (s *FiltersManager) loadPartitioned(publicKey *ecdsa.PublicKey, identity *ecdsa.PrivateKey, listen bool) (*Filter, error) { s.mutex.Lock() defer s.mutex.Unlock() @@ -237,7 +237,7 @@ func (s *FiltersManager) loadPartitioned(publicKey *ecdsa.PublicKey, listen bool // We set up a filter so we can publish, // but we discard envelopes if listen is false. - filter, err := s.addAsymmetric(chatID, listen) + filter, err := s.addAsymmetric(chatID, identity, listen) if err != nil { return nil, err } @@ -321,7 +321,7 @@ func (s *FiltersManager) LoadDiscovery() ([]*Filter, error) { OneToOne: true, } - discoveryResponse, err := s.addAsymmetric(personalDiscoveryChat.ChatID, true) + discoveryResponse, err := s.addAsymmetric(personalDiscoveryChat.ChatID, s.privateKey, true) if err != nil { return nil, err } @@ -439,7 +439,7 @@ func (s *FiltersManager) addSymmetric(chatID string) (*RawFilter, error) { // addAsymmetricFilter adds a filter with our private key // and set minPow according to the listen parameter. -func (s *FiltersManager) addAsymmetric(chatID string, listen bool) (*RawFilter, error) { +func (s *FiltersManager) addAsymmetric(chatID string, identity *ecdsa.PrivateKey, listen bool) (*RawFilter, error) { var ( err error pow = 1.0 // use PoW high enough to discard all messages for the filter @@ -452,7 +452,7 @@ func (s *FiltersManager) addAsymmetric(chatID string, listen bool) (*RawFilter, topic := ToTopic(chatID) topics := [][]byte{topic} - privateKeyID, err := s.service.AddKeyPair(s.privateKey) + privateKeyID, err := s.service.AddKeyPair(identity) if err != nil { return nil, err } diff --git a/protocol/transport/transport.go b/protocol/transport/transport.go index 6a75fe546..1d804c62e 100644 --- a/protocol/transport/transport.go +++ b/protocol/transport/transport.go @@ -35,6 +35,7 @@ type Transport interface { RemoveFilters(filters []*Filter) error ResetFilters() error Filters() []*Filter + LoadKeyFilters(*ecdsa.PrivateKey) (*Filter, error) ProcessNegotiatedSecret(secret types.NegotiatedSecret) (*Filter, error) RetrieveRawAll() (map[Filter][]*types.Message, error) } diff --git a/protocol/transport/waku/waku_service.go b/protocol/transport/waku/waku_service.go index ec4041e7a..c184a245d 100644 --- a/protocol/transport/waku/waku_service.go +++ b/protocol/transport/waku/waku_service.go @@ -204,61 +204,6 @@ func (a *Transport) LeaveGroup(publicKeys []*ecdsa.PublicKey) error { return nil } -type Message struct { - Message *types.Message - Public bool -} - -func (a *Transport) RetrieveAllMessages() ([]Message, error) { - var messages []Message - - for _, filter := range a.filters.Filters() { - filterMsgs, err := a.api.GetFilterMessages(filter.FilterID) - if err != nil { - return nil, err - } - - for _, m := range filterMsgs { - messages = append(messages, Message{ - Message: m, - Public: filter.IsPublic(), - }) - } - } - - return messages, nil -} - -func (a *Transport) RetrievePublicMessages(chatID string) ([]*types.Message, error) { - filter, err := a.filters.LoadPublic(chatID) - if err != nil { - return nil, err - } - - return a.api.GetFilterMessages(filter.FilterID) -} - -func (a *Transport) RetrievePrivateMessages(publicKey *ecdsa.PublicKey) ([]*types.Message, error) { - chats := a.filters.FiltersByPublicKey(publicKey) - discoveryChats, err := a.filters.Init(nil, nil) - if err != nil { - return nil, err - } - - var result []*types.Message - - for _, chat := range append(chats, discoveryChats...) { - filterMsgs, err := a.api.GetFilterMessages(chat.FilterID) - if err != nil { - return nil, err - } - - result = append(result, filterMsgs...) - } - - return result, nil -} - func (a *Transport) RetrieveRawAll() (map[transport.Filter][]*types.Message, error) { result := make(map[transport.Filter][]*types.Message) @@ -318,7 +263,7 @@ func (a *Transport) SendPrivateWithPartitioned(ctx context.Context, newMessage * return nil, err } - filter, err := a.filters.LoadPartitioned(publicKey) + filter, err := a.filters.LoadPartitioned(publicKey, a.keysManager.privateKey, false) if err != nil { return nil, err } @@ -329,6 +274,10 @@ func (a *Transport) SendPrivateWithPartitioned(ctx context.Context, newMessage * return a.api.Post(ctx, *newMessage) } +func (a *Transport) LoadKeyFilters(key *ecdsa.PrivateKey) (*transport.Filter, error) { + return a.filters.LoadPartitioned(&key.PublicKey, key, true) +} + func (a *Transport) SendPrivateOnDiscovery(ctx context.Context, newMessage *types.NewMessage, publicKey *ecdsa.PublicKey) ([]byte, error) { if err := a.addSig(newMessage); err != nil { return nil, err diff --git a/protocol/transport/whisper/whisper_service.go b/protocol/transport/whisper/whisper_service.go index f5c1c2382..fd043eb8b 100644 --- a/protocol/transport/whisper/whisper_service.go +++ b/protocol/transport/whisper/whisper_service.go @@ -318,7 +318,7 @@ func (a *Transport) SendPrivateWithPartitioned(ctx context.Context, newMessage * return nil, err } - filter, err := a.filters.LoadPartitioned(publicKey) + filter, err := a.filters.LoadPartitioned(publicKey, a.keysManager.privateKey, false) if err != nil { return nil, err } @@ -406,6 +406,10 @@ func (a *Transport) SendMessagesRequest( return } +func (a *Transport) LoadKeyFilters(key *ecdsa.PrivateKey) (*transport.Filter, error) { + return a.filters.LoadPartitioned(&key.PublicKey, key, true) +} + func (a *Transport) waitForRequestCompleted(ctx context.Context, requestID []byte, events chan types.EnvelopeEvent) (*types.MailServerResponse, error) { for { select { diff --git a/protocol/v1/status_message.go b/protocol/v1/status_message.go index e1653e235..1c948dd9b 100644 --- a/protocol/v1/status_message.go +++ b/protocol/v1/status_message.go @@ -37,6 +37,9 @@ type StatusMessage struct { // Hash is the transport layer hash Hash []byte `json:"-"` + // Dst is the targeted public key + Dst *ecdsa.PublicKey + // TransportLayerSigPubKey contains the public key provided by the transport layer TransportLayerSigPubKey *ecdsa.PublicKey `json:"-"` // ApplicationMetadataLayerPubKey contains the public key provided by the application metadata layer @@ -87,13 +90,25 @@ func (m *StatusMessage) HandleTransport(shhMessage *types.Message) error { m.TransportLayerSigPubKey = publicKey m.TransportPayload = shhMessage.Payload + if shhMessage.Dst != nil { + publicKey, err := crypto.UnmarshalPubkey(shhMessage.Dst) + if err != nil { + return err + } + m.Dst = publicKey + } + return nil } -func (m *StatusMessage) HandleEncryption(myKey *ecdsa.PrivateKey, senderKey *ecdsa.PublicKey, enc *encryption.Protocol) error { +func (m *StatusMessage) HandleEncryption(myKey *ecdsa.PrivateKey, senderKey *ecdsa.PublicKey, enc *encryption.Protocol, skipNegotiation bool) error { // As we handle non-encrypted messages, we make sure that DecryptPayload // is set regardless of whether this step is successful m.DecryptedPayload = m.TransportPayload + // Nothing to do + if skipNegotiation { + return nil + } var protocolMessage encryption.ProtocolMessage err := proto.Unmarshal(m.TransportPayload, &protocolMessage) @@ -319,6 +334,82 @@ func (m *StatusMessage) HandleApplication() error { return nil } + case protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION: + // This message is a bit different as it's encrypted, so we pass it straight through + + m.ParsedMessage = m.DecryptedPayload + + return nil + + case protobuf.ApplicationMetadataMessage_CONTACT_CODE_ADVERTISEMENT: + var message protobuf.ContactCodeAdvertisement + err := proto.Unmarshal(m.DecryptedPayload, &message) + if err != nil { + m.ParsedMessage = nil + log.Printf("[message::DecodeMessage] could not decode ContactCodeAdvertisement: %#x, err: %v", m.Hash, err.Error()) + } else { + m.ParsedMessage = message + + return nil + } + + case protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_REQUEST: + var message protobuf.PushNotificationRequest + err := proto.Unmarshal(m.DecryptedPayload, &message) + if err != nil { + m.ParsedMessage = nil + log.Printf("[message::DecodeMessage] could not decode PushNotificationRequest: %#x, err: %v", m.Hash, err.Error()) + } else { + m.ParsedMessage = message + + return nil + } + + case protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_REGISTRATION_RESPONSE: + var message protobuf.PushNotificationRegistrationResponse + err := proto.Unmarshal(m.DecryptedPayload, &message) + if err != nil { + m.ParsedMessage = nil + log.Printf("[message::DecodeMessage] could not decode PushNotificationRegistrationResponse: %#x, err: %v", m.Hash, err.Error()) + } else { + m.ParsedMessage = message + + return nil + } + case protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_QUERY: + var message protobuf.PushNotificationQuery + err := proto.Unmarshal(m.DecryptedPayload, &message) + if err != nil { + m.ParsedMessage = nil + log.Printf("[message::DecodeMessage] could not decode PushNotificationQuery: %#x, err: %v", m.Hash, err.Error()) + } else { + m.ParsedMessage = message + + return nil + } + case protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_QUERY_RESPONSE: + var message protobuf.PushNotificationQueryResponse + err := proto.Unmarshal(m.DecryptedPayload, &message) + if err != nil { + m.ParsedMessage = nil + log.Printf("[message::DecodeMessage] could not decode PushNotificationQueryResponse: %#x, err: %v", m.Hash, err.Error()) + } else { + m.ParsedMessage = message + + return nil + } + case protobuf.ApplicationMetadataMessage_PUSH_NOTIFICATION_RESPONSE: + var message protobuf.PushNotificationResponse + err := proto.Unmarshal(m.DecryptedPayload, &message) + if err != nil { + m.ParsedMessage = nil + log.Printf("[message::DecodeMessage] could not decode PushNotificationResponse: %#x, err: %v", m.Hash, err.Error()) + } else { + m.ParsedMessage = message + + return nil + } + } return nil } diff --git a/services/ext/api.go b/services/ext/api.go index 7da2a6537..ffb160c61 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -12,10 +12,12 @@ import ( "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/rlp" + "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/mailserver" "github.com/status-im/status-go/protocol" "github.com/status-im/status-go/protocol/encryption/multidevice" + "github.com/status-im/status-go/protocol/pushnotificationclient" "github.com/status-im/status-go/protocol/transport" "github.com/status-im/status-go/services/ext/mailservers" ) @@ -247,6 +249,14 @@ func (api *PublicAPI) DeleteChat(parent context.Context, chatID string) error { return api.service.messenger.DeleteChat(chatID) } +func (api *PublicAPI) MuteChat(parent context.Context, chatID string) error { + return api.service.messenger.MuteChat(chatID) +} + +func (api *PublicAPI) UnmuteChat(parent context.Context, chatID string) error { + return api.service.messenger.UnmuteChat(chatID) +} + func (api *PublicAPI) SaveContact(parent context.Context, contact *protocol.Contact) error { return api.service.messenger.SaveContact(contact) } @@ -393,6 +403,114 @@ func (api *PublicAPI) UpdateMailservers(enodes []string) error { return api.service.UpdateMailservers(nodes) } +// PushNotifications server endpoints + +func (api *PublicAPI) StartPushNotificationsServer() error { + err := api.service.accountsDB.SaveSetting("push-notifications-server-enabled?", true) + if err != nil { + return err + } + + return api.service.messenger.StartPushNotificationsServer() +} + +func (api *PublicAPI) StopPushNotificationsServer() error { + err := api.service.accountsDB.SaveSetting("push-notifications-server-enabled?", false) + if err != nil { + return err + } + + return api.service.messenger.StopPushNotificationsServer() +} + +// PushNotification client endpoints + +func (api *PublicAPI) RegisterForPushNotifications(ctx context.Context, deviceToken string) error { + // We set both for now as they are equivalent + err := api.service.accountsDB.SaveSetting("remote-push-notifications-enabled?", true) + if err != nil { + return err + } + err = api.service.accountsDB.SaveSetting("notifications-enabled?", true) + if err != nil { + return err + } + + return api.service.messenger.RegisterForPushNotifications(ctx, deviceToken) +} + +func (api *PublicAPI) UnregisterForPushNotifications(ctx context.Context) error { + err := api.service.accountsDB.SaveSetting("remote-push-notifications-enabled?", false) + if err != nil { + return err + } + err = api.service.accountsDB.SaveSetting("notifications-enabled?", false) + if err != nil { + return err + } + + return api.service.messenger.UnregisterFromPushNotifications(ctx) +} + +func (api *PublicAPI) DisableSendingNotifications(ctx context.Context) error { + err := api.service.accountsDB.SaveSetting("send-push-notifications?", false) + if err != nil { + return err + } + + return api.service.messenger.DisableSendingPushNotifications() +} + +func (api *PublicAPI) EnableSendingNotifications(ctx context.Context) error { + err := api.service.accountsDB.SaveSetting("send-push-notifications?", true) + if err != nil { + return err + } + return api.service.messenger.EnableSendingPushNotifications() +} + +func (api *PublicAPI) EnablePushNotificationsFromContactsOnly(ctx context.Context) error { + err := api.service.accountsDB.SaveSetting("push-notifications-from-contacts-only?", true) + if err != nil { + return err + } + return api.service.messenger.EnablePushNotificationsFromContactsOnly() +} + +func (api *PublicAPI) DisablePushNotificationsFromContactsOnly(ctx context.Context) error { + err := api.service.accountsDB.SaveSetting("push-notifications-from-contacts-only?", false) + if err != nil { + return err + } + return api.service.messenger.DisablePushNotificationsFromContactsOnly() +} + +func (api *PublicAPI) AddPushNotificationsServer(ctx context.Context, publicKeyBytes types.HexBytes) error { + publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes) + if err != nil { + return err + } + + return api.service.messenger.AddPushNotificationsServer(ctx, publicKey) +} + +func (api *PublicAPI) RemovePushNotificationServer(ctx context.Context, publicKeyBytes types.HexBytes) error { + publicKey, err := crypto.UnmarshalPubkey(publicKeyBytes) + if err != nil { + return err + } + + return api.service.messenger.RemovePushNotificationServer(ctx, publicKey) +} + +func (api *PublicAPI) GetPushNotificationServers() ([]*pushnotificationclient.PushNotificationServer, error) { + return api.service.messenger.GetPushNotificationServers() +} + +func (api *PublicAPI) RegisteredForPushNotifications() (bool, error) { + return api.service.messenger.RegisteredForPushNotifications() +} + // Echo is a method for testing purposes. func (api *PublicAPI) Echo(ctx context.Context, message string) (string, error) { return message, nil diff --git a/services/ext/service.go b/services/ext/service.go index b691537d2..c39741c0c 100644 --- a/services/ext/service.go +++ b/services/ext/service.go @@ -33,6 +33,8 @@ import ( coretypes "github.com/status-im/status-go/eth-node/core/types" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/protocol" + "github.com/status-im/status-go/protocol/pushnotificationclient" + "github.com/status-im/status-go/protocol/pushnotificationserver" "github.com/status-im/status-go/protocol/transport" ) @@ -144,7 +146,12 @@ func (s *Service) InitProtocol(identity *ecdsa.PrivateKey, db *sql.DB, logger *z EnvelopeEventsHandler: EnvelopeSignalHandler{}, Logger: logger, } - options := buildMessengerOptions(s.config, db, envelopesMonitorConfig, logger) + s.accountsDB = accounts.NewDB(db) + + options, err := buildMessengerOptions(s.config, identity, db, envelopesMonitorConfig, s.accountsDB, logger) + if err != nil { + return err + } messenger, err := protocol.NewMessenger( identity, @@ -155,7 +162,6 @@ func (s *Service) InitProtocol(identity *ecdsa.PrivateKey, db *sql.DB, logger *z if err != nil { return err } - s.accountsDB = accounts.NewDB(db) s.messenger = messenger return messenger.Init() } @@ -338,6 +344,9 @@ func (s *Service) DisableInstallation(installationID string) error { // UpdateMailservers updates information about selected mail servers. func (s *Service) UpdateMailservers(nodes []*enode.Node) error { + if len(nodes) > 0 && s.messenger != nil { + s.messenger.SetMailserver(nodes[0].ID().Bytes()) + } if err := s.peerStore.Update(nodes); err != nil { return err } @@ -439,10 +448,12 @@ func onNegotiatedFilters(filters []*transport.Filter) { func buildMessengerOptions( config params.ShhextConfig, + identity *ecdsa.PrivateKey, db *sql.DB, envelopesMonitorConfig *transport.EnvelopesMonitorConfig, + accountsDB *accounts.Database, logger *zap.Logger, -) []protocol.Option { +) ([]protocol.Option, error) { options := []protocol.Option{ protocol.WithCustomLogger(logger), protocol.WithDatabase(db), @@ -453,6 +464,23 @@ func buildMessengerOptions( if config.DataSyncEnabled { options = append(options, protocol.WithDatasync()) } + settings, err := accountsDB.GetSettings() + if err != sql.ErrNoRows && err != nil { + return nil, err + } + + if settings.PushNotificationsServerEnabled { + config := &pushnotificationserver.Config{ + Logger: logger, + } + options = append(options, protocol.WithPushNotificationServerConfig(config)) + } + + options = append(options, protocol.WithPushNotificationClientConfig(&pushnotificationclient.Config{ + SendEnabled: settings.SendPushNotifications, + AllowFromContactsOnly: settings.PushNotificationsFromContactsOnly, + RemoteNotificationsEnabled: settings.RemotePushNotificationsEnabled, + })) if config.VerifyTransactionURL != "" { client := &verifyTransactionClient{ @@ -462,5 +490,5 @@ func buildMessengerOptions( options = append(options, protocol.WithVerifyTransactionClient(client)) } - return options + return options, nil } diff --git a/services/shhext/api_geth_test.go b/services/shhext/api_geth_test.go index 30d5ed990..b33b454b3 100644 --- a/services/shhext/api_geth_test.go +++ b/services/shhext/api_geth_test.go @@ -24,12 +24,12 @@ import ( "github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb/storage" + "github.com/status-im/status-go/appdatabase" gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/params" "github.com/status-im/status-go/services/ext" - "github.com/status-im/status-go/sqlite" "github.com/status-im/status-go/t/helpers" "github.com/status-im/status-go/whisper/v6" ) @@ -209,7 +209,7 @@ func TestInitProtocol(t *testing.T) { tmpdir, err := ioutil.TempDir("", "test-shhext-service-init-protocol") require.NoError(t, err) - sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/db.sql", tmpdir), "password") + sqlDB, err := appdatabase.InitializeDB(fmt.Sprintf("%s/db.sql", tmpdir), "password") require.NoError(t, err) err = service.InitProtocol(privateKey, sqlDB, zap.NewNop()) @@ -262,7 +262,7 @@ func (s *ShhExtSuite) createAndAddNode() { s.Require().NoError(err) nodeWrapper := ext.NewTestNodeWrapper(gethbridge.NewGethWhisperWrapper(whisper), nil) service := New(config, nodeWrapper, nil, nil, db) - sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/%d", s.dir, idx), "password") + sqlDB, err := appdatabase.InitializeDB(fmt.Sprintf("%s/%d", s.dir, idx), "password") s.Require().NoError(err) privateKey, err := crypto.GenerateKey() s.NoError(err) diff --git a/services/wakuext/api_test.go b/services/wakuext/api_test.go index a8fb1a894..71231602d 100644 --- a/services/wakuext/api_test.go +++ b/services/wakuext/api_test.go @@ -23,12 +23,12 @@ import ( "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p/enode" + "github.com/status-im/status-go/appdatabase" gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/params" "github.com/status-im/status-go/services/ext" - "github.com/status-im/status-go/sqlite" "github.com/status-im/status-go/t/helpers" "github.com/status-im/status-go/waku" wakucommon "github.com/status-im/status-go/waku/common" @@ -126,7 +126,7 @@ func TestInitProtocol(t *testing.T) { tmpdir, err := ioutil.TempDir("", "test-shhext-service-init-protocol") require.NoError(t, err) - sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/db.sql", tmpdir), "password") + sqlDB, err := appdatabase.InitializeDB(fmt.Sprintf("%s/db.sql", tmpdir), "password") require.NoError(t, err) err = service.InitProtocol(privateKey, sqlDB, zap.NewNop()) @@ -179,7 +179,7 @@ func (s *ShhExtSuite) createAndAddNode() { s.Require().NoError(err) nodeWrapper := ext.NewTestNodeWrapper(nil, gethbridge.NewGethWakuWrapper(w)) service := New(config, nodeWrapper, nil, nil, db) - sqlDB, err := sqlite.OpenDB(fmt.Sprintf("%s/%d", s.dir, idx), "password") + sqlDB, err := appdatabase.InitializeDB(fmt.Sprintf("%s/%d", s.dir, idx), "password") s.Require().NoError(err) privateKey, err := crypto.GenerateKey() s.NoError(err) diff --git a/static/bindata.go b/static/bindata.go index c11101613..86f0a3b07 100644 --- a/static/bindata.go +++ b/static/bindata.go @@ -97,7 +97,7 @@ func ConfigReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/README.md", size: 3132, mode: os.FileMode(0644), modTime: time.Unix(1584434371, 0)} + info := bindataFileInfo{name: "../config/README.md", size: 3132, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xd0, 0x71, 0x14, 0x3e, 0x9d, 0x9c, 0x11, 0x40, 0xe1, 0xe9, 0x8b, 0xcc, 0x38, 0x17, 0x69, 0xb, 0xa7, 0xf2, 0x91, 0xa6, 0x4a, 0x9f, 0xd2, 0xc6, 0xf4, 0xff, 0x37, 0x5d, 0xd3, 0xed, 0x7c, 0xbd}} return a, nil } @@ -117,7 +117,7 @@ func ConfigCliFleetEthProdJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/cli/fleet-eth.prod.json", size: 3625, mode: os.FileMode(0644), modTime: time.Unix(1585559578, 0)} + info := bindataFileInfo{name: "../config/cli/fleet-eth.prod.json", size: 3625, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x1c, 0x86, 0x93, 0x3b, 0x8c, 0xb4, 0x1b, 0x8b, 0x70, 0xe6, 0x90, 0x3, 0xc9, 0x93, 0x99, 0x97, 0x18, 0xb7, 0x49, 0xbf, 0x2d, 0x35, 0x98, 0xb8, 0x49, 0xa8, 0x73, 0x30, 0xf5, 0x2d, 0x7f, 0x5c}} return a, nil } @@ -137,7 +137,7 @@ func ConfigCliFleetEthStagingJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/cli/fleet-eth.staging.json", size: 1846, mode: os.FileMode(0644), modTime: time.Unix(1585559578, 0)} + info := bindataFileInfo{name: "../config/cli/fleet-eth.staging.json", size: 1846, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xd1, 0xef, 0x3d, 0x5c, 0x41, 0xb1, 0xc8, 0x54, 0xd3, 0x82, 0x60, 0xcf, 0x5f, 0x8b, 0x92, 0xd0, 0x41, 0xa7, 0xff, 0xfd, 0x2c, 0x95, 0x44, 0x68, 0xd3, 0xad, 0x16, 0x8c, 0x49, 0x10, 0x4e, 0xb7}} return a, nil } @@ -157,7 +157,7 @@ func ConfigCliFleetEthTestJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "../config/cli/fleet-eth.test.json", size: 1855, mode: os.FileMode(0644), modTime: time.Unix(1585559578, 0)} + info := bindataFileInfo{name: "../config/cli/fleet-eth.test.json", size: 1855, mode: os.FileMode(0644), modTime: time.Unix(1595493797, 0)} a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xe4, 0x90, 0x4, 0x6, 0x2b, 0xe4, 0x67, 0x7a, 0xfc, 0x70, 0xa9, 0xb, 0x85, 0x8c, 0xb3, 0x8, 0x8e, 0xee, 0xec, 0x64, 0xb7, 0xac, 0x4c, 0x6, 0x4d, 0xcc, 0x4f, 0xda, 0xbd, 0xed, 0x17, 0x53}} return a, nil }