From 8e95282057e35598d778ffe9bcae7502ce648e9b Mon Sep 17 00:00:00 2001 From: Jonathan Rainville Date: Thu, 17 Apr 2025 13:30:22 -0400 Subject: [PATCH] feat(news)_: hook News settings to the newsFeedManager and its polling Needed for https://github.com/status-im/status-desktop/issues/17811 Adds the NewsRSSEnabled setting that is going to be used in the Privacy settings of the clients. Adds API methods to toggle the NewsFeedEnabled and NewsRSSEnabled settings. By doing so, it will trigger the NewsFeedManager polling if both settings are set to true. Conversely, if one of them is disabled, it stops the polling. --- .../1744905192_add_news_rss_setting.up.sql | 1 + internal/newsfeed/new_feed_manager_test.go | 6 ++- internal/newsfeed/news_feed_manager.go | 19 ++++++- multiaccounts/settings/columns.go | 6 +++ multiaccounts/settings/database.go | 27 +++++++++- .../settings/database_settings_manager.go | 3 ++ multiaccounts/settings/database_test.go | 31 ++++++++--- multiaccounts/settings/structs.go | 1 + protocol/messenger.go | 9 +++- protocol/messenger_news_feed.go | 51 +++++++++++++++++++ protocol/messenger_news_feed_test.go | 41 ++++++++++++++- services/accounts/settings.go | 13 +++++ services/ext/api.go | 10 ++++ 13 files changed, 203 insertions(+), 15 deletions(-) create mode 100644 appdatabase/migrations/sql/1744905192_add_news_rss_setting.up.sql diff --git a/appdatabase/migrations/sql/1744905192_add_news_rss_setting.up.sql b/appdatabase/migrations/sql/1744905192_add_news_rss_setting.up.sql new file mode 100644 index 00000000000..399cac35310 --- /dev/null +++ b/appdatabase/migrations/sql/1744905192_add_news_rss_setting.up.sql @@ -0,0 +1 @@ +ALTER TABLE settings ADD COLUMN news_rss_enabled BOOLEAN DEFAULT TRUE; diff --git a/internal/newsfeed/new_feed_manager_test.go b/internal/newsfeed/new_feed_manager_test.go index 06b5afff536..1aa6551bfe7 100644 --- a/internal/newsfeed/new_feed_manager_test.go +++ b/internal/newsfeed/new_feed_manager_test.go @@ -149,13 +149,15 @@ func (s *NewsFeedManagerSuite) TestStartAndStopFetching() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - newsFeedManager.StartFetching(ctx) + newsFeedManager.StartPolling(ctx) time.Sleep(1 * time.Millisecond) // Leave time for the go routine to run and process // The start fetching does an initial fetch immediately s.Require().Len(captured, 1) s.Require().Equal("New Item", captured[0].Title) + s.Require().True(newsFeedManager.IsPolling()) - newsFeedManager.StopFetching() + newsFeedManager.StopPolling() + s.Require().False(newsFeedManager.IsPolling()) } diff --git a/internal/newsfeed/news_feed_manager.go b/internal/newsfeed/news_feed_manager.go index 76b0325771d..b70b156960d 100644 --- a/internal/newsfeed/news_feed_manager.go +++ b/internal/newsfeed/news_feed_manager.go @@ -27,6 +27,7 @@ type NewsFeedManager struct { handler FeedHandler fetchFrom time.Time pollingInterval time.Duration + polling bool cancel context.CancelFunc logger *zap.Logger } @@ -73,6 +74,7 @@ func NewNewsFeedManager(opts ...Option) *NewsFeedManager { nfm := &NewsFeedManager{ pollingInterval: time.Minute * 30, fetchFrom: time.Now(), + polling: false, } for _, opt := range opts { @@ -122,7 +124,16 @@ func (n *NewsFeedManager) fetchRSSAndHandle() error { return nil } -func (n *NewsFeedManager) StartFetching(ctx context.Context) { +func (n *NewsFeedManager) IsPolling() bool { + return n.polling +} + +func (n *NewsFeedManager) StartPolling(ctx context.Context) { + if n.polling { + return + } + n.polling = true + // Derive the given context, save the CancelFunc ctx, n.cancel = context.WithCancel(ctx) @@ -145,6 +156,10 @@ func (n *NewsFeedManager) StartFetching(ctx context.Context) { }() } -func (n *NewsFeedManager) StopFetching() { +func (n *NewsFeedManager) StopPolling() { + if !n.polling { + return + } n.cancel() + n.polling = false } diff --git a/multiaccounts/settings/columns.go b/multiaccounts/settings/columns.go index 6d02006048c..28cc8b93c1b 100644 --- a/multiaccounts/settings/columns.go +++ b/multiaccounts/settings/columns.go @@ -242,6 +242,11 @@ var ( dBColumnName: "news_notifications_enabled", valueHandler: BoolHandler, } + NewsRSSEnabled = SettingField{ + reactFieldName: "news-rss-enabled?", + dBColumnName: "news_rss_enabled", + valueHandler: BoolHandler, + } NodeConfig = SettingField{ reactFieldName: "node-config", dBColumnName: "node_config", @@ -582,6 +587,7 @@ var ( NewsFeedEnabled, NewsFeedLastFetchedTimestamp, NewsNotificationsEnabled, + NewsRSSEnabled, MessengerNotificationsEnabled, NodeConfig, NotificationsEnabled, diff --git a/multiaccounts/settings/database.go b/multiaccounts/settings/database.go index 79fa30de727..4ca7d9ecbed 100644 --- a/multiaccounts/settings/database.go +++ b/multiaccounts/settings/database.go @@ -396,7 +396,7 @@ func (db *Database) GetSettings() (Settings, error) { test_networks_enabled, mutual_contact_enabled, profile_migration_needed, wallet_token_preferences_group_by_community, url_unfurling_mode, mnemonic_was_not_shown, wallet_show_community_asset_when_sending_tokens, wallet_display_assets_below_balance, wallet_display_assets_below_balance_threshold, wallet_collectible_preferences_group_by_collection, wallet_collectible_preferences_group_by_community, - peer_syncing_enabled, auto_refresh_tokens_enabled, last_tokens_update, news_feed_enabled, news_feed_last_fetched_timestamp + peer_syncing_enabled, auto_refresh_tokens_enabled, last_tokens_update, news_feed_enabled, news_feed_last_fetched_timestamp, news_rss_enabled FROM settings WHERE @@ -485,6 +485,7 @@ func (db *Database) GetSettings() (Settings, error) { &lastTokensUpdate, &s.NewsFeedEnabled, &newsFeedLastFetchedTimestamp, + &s.NewsRSSEnabled, ) if err != nil { @@ -902,3 +903,27 @@ func (db *Database) NewsFeedLastFetchedTimestamp() (result time.Time, err error) } return } + +func (db *Database) NewsFeedEnabled() (result bool, err error) { + err = db.makeSelectRow(NewsFeedEnabled).Scan(&result) + if err == sql.ErrNoRows { + return result, nil + } + return result, err +} + +func (db *Database) NewsNotificationsEnabled() (result bool, err error) { + err = db.makeSelectRow(NewsNotificationsEnabled).Scan(&result) + if err == sql.ErrNoRows { + return result, nil + } + return result, err +} + +func (db *Database) NewsRSSEnabled() (result bool, err error) { + err = db.makeSelectRow(NewsRSSEnabled).Scan(&result) + if err == sql.ErrNoRows { + return result, nil + } + return result, err +} diff --git a/multiaccounts/settings/database_settings_manager.go b/multiaccounts/settings/database_settings_manager.go index aa7533b3489..1031d3bb770 100644 --- a/multiaccounts/settings/database_settings_manager.go +++ b/multiaccounts/settings/database_settings_manager.go @@ -84,4 +84,7 @@ type DatabaseSettingsManager interface { AutoRefreshTokensEnabled() (result bool, err error) LastTokensUpdate() (result time.Time, err error) NewsFeedLastFetchedTimestamp() (result time.Time, err error) + NewsFeedEnabled() (result bool, err error) + NewsNotificationsEnabled() (result bool, err error) + NewsRSSEnabled() (result bool, err error) } diff --git a/multiaccounts/settings/database_test.go b/multiaccounts/settings/database_test.go index ebe694dbeec..6436f095642 100644 --- a/multiaccounts/settings/database_test.go +++ b/multiaccounts/settings/database_test.go @@ -50,6 +50,7 @@ var ( DisplayAssetsBelowBalance: false, ShowCommunityAssetWhenSendingTokens: true, NewsFeedEnabled: true, + NewsRSSEnabled: true, } ) @@ -208,9 +209,9 @@ func TestDatabase_NewsNotificationsEnabled(t *testing.T) { require.NoError(t, db.CreateSettings(settings, config)) - settings, err := db.GetSettings() + enabled, err := db.NewsNotificationsEnabled() require.NoError(t, err) - require.Equal(t, false, settings.NewsNotificationsEnabled) + require.Equal(t, false, enabled) err = db.SaveSetting(NewsNotificationsEnabled.GetReactName(), true) require.NoError(t, err) @@ -244,10 +245,10 @@ func TestDatabase_NewsFeedEnabled(t *testing.T) { require.NoError(t, db.CreateSettings(settings, config)) - settings, err := db.GetSettings() + enabled, err := db.NewsFeedEnabled() require.NoError(t, err) - require.Equal(t, true, settings.NewsFeedEnabled) + require.Equal(t, true, enabled) err = db.SaveSetting(NewsFeedEnabled.GetReactName(), false) require.NoError(t, err) @@ -263,11 +264,11 @@ func TestDatabase_NewsFeedLastFetchedTimestamp(t *testing.T) { require.NoError(t, db.CreateSettings(settings, config)) - settings, err := db.GetSettings() + timestamp, err := db.NewsFeedLastFetchedTimestamp() require.NoError(t, err) // Using GreaterOrEqual because the timestamp is set during CreateSettings and there is a tiny chance that the second changes between - require.GreaterOrEqual(t, time.Now().UTC().Second(), settings.NewsFeedLastFetchedTimestamp.UTC().Second()) + require.GreaterOrEqual(t, time.Now().UTC().Second(), timestamp.UTC().Second()) dateNow := time.Now() err = db.SaveSetting(NewsFeedLastFetchedTimestamp.GetReactName(), dateNow) @@ -277,3 +278,21 @@ func TestDatabase_NewsFeedLastFetchedTimestamp(t *testing.T) { require.NoError(t, err) require.Equal(t, dateNow.UTC().Second(), settings.NewsFeedLastFetchedTimestamp.UTC().Second()) } + +func TestDatabase_NewsRSSEnabled(t *testing.T) { + db, stop := setupTestDB(t) + defer stop() + + require.NoError(t, db.CreateSettings(settings, config)) + + enabled, err := db.NewsRSSEnabled() + require.NoError(t, err) + require.Equal(t, true, enabled) + + err = db.SaveSetting(NewsRSSEnabled.GetReactName(), false) + require.NoError(t, err) + + settings, err = db.GetSettings() + require.NoError(t, err) + require.Equal(t, false, settings.NewsRSSEnabled) +} diff --git a/multiaccounts/settings/structs.go b/multiaccounts/settings/structs.go index fbee18559e5..d9f9fb10a47 100644 --- a/multiaccounts/settings/structs.go +++ b/multiaccounts/settings/structs.go @@ -161,6 +161,7 @@ type Settings struct { NewsFeedEnabled bool `json:"news-feed-enabled?,omitempty"` NewsFeedLastFetchedTimestamp time.Time `json:"news-feed-last-fetched-timestamp,omitempty"` NewsNotificationsEnabled bool `json:"news-notifications-enabled?,omitempty"` + NewsRSSEnabled bool `json:"news-rss-enabled?,omitempty"` PhotoPath string `json:"photo-path"` PinnedMailserver *json.RawMessage `json:"pinned-mailservers,omitempty"` // PreferredName represents the user's preferred Ethereum Name Service (ENS) name. diff --git a/protocol/messenger.go b/protocol/messenger.go index 08c4739b90c..6bf52058057 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -912,8 +912,13 @@ func (m *Messenger) Start() (*MessengerResponse, error) { newsfeed.WithFetchFrom(lastFetched), ) - // TODO only start if the setting is enabled - m.newsFeedManager.StartFetching(m.ctx) + newsFeedEnabled, err := m.IsNewsFeedEnabled() + if err != nil { + return nil, err + } + if newsFeedEnabled { + m.newsFeedManager.StartPolling(m.ctx) + } } return response, nil diff --git a/protocol/messenger_news_feed.go b/protocol/messenger_news_feed.go index 60a8f0af2cc..db6bc57d17b 100644 --- a/protocol/messenger_news_feed.go +++ b/protocol/messenger_news_feed.go @@ -91,3 +91,54 @@ func (m *Messenger) FetchNewsMessages() (*MessengerResponse, error) { } return response, nil } + +// The News Feed is enabled if both: +// 1. The News Feed is enabled in the settings. +// 2. The RSS feed is enabled in the settings. +func (m *Messenger) IsNewsFeedEnabled() (bool, error) { + newsFeedEnabled, err := m.settings.NewsFeedEnabled() + if err != nil { + return false, err + } + if !newsFeedEnabled { + return false, nil + } + newsRSSEnabled, err := m.settings.NewsRSSEnabled() + if err != nil { + return false, err + } + return newsRSSEnabled, nil +} + +func (m *Messenger) changeNewsFeedManagerAfterUpdate() error { + if m.newsFeedManager == nil { + return nil + } + isNewsFeedEnabled, err := m.IsNewsFeedEnabled() + if err != nil { + return err + } + + if isNewsFeedEnabled { + m.newsFeedManager.StartPolling(m.ctx) + } else { + m.newsFeedManager.StopPolling() + } + return nil +} + +func (m *Messenger) ToggleNewsFeedEnabled(value bool) error { + err := m.settings.SaveSetting(settings.NewsFeedEnabled.GetReactName(), value) + if err != nil { + return err + } + return m.changeNewsFeedManagerAfterUpdate() +} + +func (m *Messenger) ToggleNewsRSSEnabled(value bool) error { + err := m.settings.SaveSetting(settings.NewsRSSEnabled.GetReactName(), value) + if err != nil { + return err + } + return m.changeNewsFeedManagerAfterUpdate() +} diff --git a/protocol/messenger_news_feed_test.go b/protocol/messenger_news_feed_test.go index c1d44f16a72..d648ee73f0c 100644 --- a/protocol/messenger_news_feed_test.go +++ b/protocol/messenger_news_feed_test.go @@ -4,10 +4,11 @@ import ( "testing" "time" - "github.com/stretchr/testify/suite" - "github.com/brianvoe/gofakeit/v6" "github.com/mmcdole/gofeed" + "github.com/stretchr/testify/suite" + + "github.com/status-im/status-go/internal/newsfeed" ) type MessengerNewsFeedSuite struct { @@ -19,6 +20,14 @@ func (s *MessengerNewsFeedSuite) SetupTest() { s.MessengerBaseTestSuite.SetupTest() s.m = s.newMessenger() + s.m.newsFeedManager = newsfeed.NewNewsFeedManager( + newsfeed.WithURL(newsfeed.STATUS_FEED_URL), + newsfeed.WithParser(gofeed.NewParser()), + newsfeed.WithHandler(s.m), + newsfeed.WithLogger(s.m.logger), + newsfeed.WithPollingInterval(30*time.Minute), + newsfeed.WithFetchFrom(time.Now().Add(-1*time.Hour)), + ) } func TestMessengerNewsFeedSuite(t *testing.T) { @@ -55,3 +64,31 @@ func (s *MessengerNewsFeedSuite) TestHandleNewsFeedItem() { s.Require().Len(notifications, 1) s.Require().Equal(item.Title, notifications[0].NewsTitle) } + +func (s *MessengerNewsFeedSuite) TestToggleSettings() { + err := s.m.ToggleNewsFeedEnabled(true) + s.Require().NoError(err) + s.Require().True(s.m.newsFeedManager.IsPolling()) + + // Polling is off as soon as one setting is off + err = s.m.ToggleNewsFeedEnabled(false) + s.Require().NoError(err) + s.Require().False(s.m.newsFeedManager.IsPolling()) + + err = s.m.ToggleNewsRSSEnabled(false) + s.Require().NoError(err) + s.Require().False(s.m.newsFeedManager.IsPolling()) + + //Poolling is still off + err = s.m.ToggleNewsRSSEnabled(true) + s.Require().NoError(err) + s.Require().False(s.m.newsFeedManager.IsPolling()) + + // Polling restart if both settings are on + err = s.m.ToggleNewsFeedEnabled(true) + s.Require().NoError(err) + s.Require().True(s.m.newsFeedManager.IsPolling()) + + s.m.newsFeedManager.StopPolling() + s.Require().False(s.m.newsFeedManager.IsPolling()) +} diff --git a/services/accounts/settings.go b/services/accounts/settings.go index 60d7278cd4a..cbe3f67e849 100644 --- a/services/accounts/settings.go +++ b/services/accounts/settings.go @@ -50,6 +50,19 @@ func (api *SettingsAPI) SaveNodeConfig(ctx context.Context, n *params.NodeConfig return nodecfg.SaveNodeConfig(api.db.DB(), n) } +// News Settings +func (api *SettingsAPI) NewsFeedEnabled() (bool, error) { + return api.db.NewsFeedEnabled() +} + +func (api *SettingsAPI) NewsNotificationsEnabled() (bool, error) { + return api.db.NewsNotificationsEnabled() +} + +func (api *SettingsAPI) NewsRSSEnabled() (bool, error) { + return api.db.NewsRSSEnabled() +} + // Notifications Settings func (api *SettingsAPI) NotificationsGetAllowNotifications() (bool, error) { return api.db.GetAllowNotifications() diff --git a/services/ext/api.go b/services/ext/api.go index da7ffdd102c..3d77f04ed8c 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -1362,6 +1362,16 @@ func (api *PublicAPI) FetchNewsMessages() (*protocol.MessengerResponse, error) { return m.FetchNewsMessages() } +func (api *PublicAPI) ToggleNewsFeedEnabled(value bool) error { + m := api.service.messenger + return m.ToggleNewsFeedEnabled(value) +} + +func (api *PublicAPI) ToggleNewsRSSEnabled(value bool) error { + m := api.service.messenger + return m.ToggleNewsRSSEnabled(value) +} + func (api *PublicAPI) RequestAllHistoricMessages(forceFetchingBackup bool) (*protocol.MessengerResponse, error) { return api.service.messenger.RequestAllHistoricMessages(forceFetchingBackup, false) }