1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "miscellaneous/feedreader.h"
4 
5 #include "3rd-party/boolinq/boolinq.h"
6 #include "core/feeddownloader.h"
7 #include "core/feedsmodel.h"
8 #include "core/feedsproxymodel.h"
9 #include "core/messagesmodel.h"
10 #include "core/messagesproxymodel.h"
11 #include "database/databasequeries.h"
12 #include "gui/dialogs/formmessagefiltersmanager.h"
13 #include "miscellaneous/application.h"
14 #include "miscellaneous/mutex.h"
15 #include "services/abstract/cacheforserviceroot.h"
16 #include "services/abstract/serviceroot.h"
17 #include "services/feedly/feedlyentrypoint.h"
18 #include "services/gmail/gmailentrypoint.h"
19 #include "services/greader/greaderentrypoint.h"
20 #include "services/owncloud/owncloudserviceentrypoint.h"
21 #include "services/standard/standardserviceentrypoint.h"
22 #include "services/tt-rss/ttrssserviceentrypoint.h"
23 
24 #include <QThread>
25 #include <QTimer>
26 
FeedReader(QObject * parent)27 FeedReader::FeedReader(QObject* parent)
28   : QObject(parent),
29   m_autoUpdateTimer(new QTimer(this)), m_feedDownloader(nullptr) {
30   m_feedsModel = new FeedsModel(this);
31   m_feedsProxyModel = new FeedsProxyModel(m_feedsModel, this);
32   m_messagesModel = new MessagesModel(this);
33   m_messagesProxyModel = new MessagesProxyModel(m_messagesModel, this);
34 
35   connect(m_autoUpdateTimer, &QTimer::timeout, this, &FeedReader::executeNextAutoUpdate);
36   updateAutoUpdateStatus();
37   initializeFeedDownloader();
38 
39   if (qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::FeedsUpdateOnStartup)).toBool()) {
40     qDebugNN << LOGSEC_CORE
41              << "Requesting update for all feeds on application startup.";
42     QTimer::singleShot(qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::FeedsUpdateStartupDelay)).toDouble() * 1000,
43                        this,
44                        [this]() {
45       updateFeeds(m_feedsModel->rootItem()->getSubAutoFetchingEnabledFeeds());
46     });
47   }
48 }
49 
~FeedReader()50 FeedReader::~FeedReader() {
51   qDebugNN << LOGSEC_CORE << "Destroying FeedReader instance.";
52   qDeleteAll(m_feedServices);
53   qDeleteAll(m_messageFilters);
54 }
55 
feedServices()56 QList<ServiceEntryPoint*> FeedReader::feedServices() {
57   if (m_feedServices.isEmpty()) {
58     // NOTE: All installed services create their entry points here.
59     m_feedServices.append(new FeedlyEntryPoint());
60     m_feedServices.append(new GmailEntryPoint());
61     m_feedServices.append(new GreaderEntryPoint());
62     m_feedServices.append(new OwnCloudServiceEntryPoint());
63     m_feedServices.append(new StandardServiceEntryPoint());
64     m_feedServices.append(new TtRssServiceEntryPoint());
65   }
66 
67   return m_feedServices;
68 }
69 
updateFeeds(const QList<Feed * > & feeds)70 void FeedReader::updateFeeds(const QList<Feed*>& feeds) {
71   if (!qApp->feedUpdateLock()->tryLock()) {
72     qApp->showGuiMessage(Notification::Event::GeneralEvent,
73                          tr("Cannot fetch articles at this point"),
74                          tr("You cannot fetch new articles now because another critical operation is ongoing."),
75                          QSystemTrayIcon::MessageIcon::Warning,
76                          true);
77     return;
78   }
79 
80   QMetaObject::invokeMethod(m_feedDownloader, "updateFeeds",
81                             Qt::ConnectionType::QueuedConnection,
82                             Q_ARG(QList<Feed*>, feeds));
83 }
84 
synchronizeMessageData(const QList<CacheForServiceRoot * > & caches)85 void FeedReader::synchronizeMessageData(const QList<CacheForServiceRoot*>& caches) {
86   QMetaObject::invokeMethod(m_feedDownloader, "synchronizeAccountCaches",
87                             Qt::ConnectionType::QueuedConnection,
88                             Q_ARG(QList<CacheForServiceRoot*>, caches),
89                             Q_ARG(bool, true));
90 }
91 
initializeFeedDownloader()92 void FeedReader::initializeFeedDownloader() {
93   if (m_feedDownloader == nullptr) {
94     qDebugNN << LOGSEC_CORE << "Creating FeedDownloader singleton.";
95 
96     m_feedDownloader = new FeedDownloader();
97     m_feedDownloaderThread = new QThread();
98 
99     // Downloader setup.
100     qRegisterMetaType<QList<Feed*>>("QList<Feed*>");
101     qRegisterMetaType<QList<CacheForServiceRoot*>>("QList<CacheForServiceRoot*>");
102 
103     m_feedDownloader->moveToThread(m_feedDownloaderThread);
104 
105     connect(m_feedDownloaderThread, &QThread::finished, m_feedDownloaderThread, &QThread::deleteLater);
106     connect(m_feedDownloaderThread, &QThread::finished, m_feedDownloader, &FeedDownloader::deleteLater);
107     connect(m_feedDownloader, &FeedDownloader::updateFinished, this, &FeedReader::feedUpdatesFinished);
108     connect(m_feedDownloader, &FeedDownloader::updateProgress, this, &FeedReader::feedUpdatesProgress);
109     connect(m_feedDownloader, &FeedDownloader::updateStarted, this, &FeedReader::feedUpdatesStarted);
110     connect(m_feedDownloader, &FeedDownloader::updateFinished, qApp->feedUpdateLock(), &Mutex::unlock);
111 
112     m_feedDownloaderThread->start();
113   }
114 }
115 
showMessageFiltersManager()116 void FeedReader::showMessageFiltersManager() {
117   FormMessageFiltersManager manager(qApp->feedReader(),
118                                     qApp->feedReader()->feedsModel()->serviceRoots(),
119                                     qApp->mainFormWidget());
120 
121   manager.exec();
122 
123   m_messagesModel->reloadWholeLayout();
124 }
125 
updateAutoUpdateStatus()126 void FeedReader::updateAutoUpdateStatus() {
127   // Restore global intervals.
128   // NOTE: Specific per-feed interval are left intact.
129   m_globalAutoUpdateInitialInterval = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::AutoUpdateInterval)).toInt();
130   m_globalAutoUpdateRemainingInterval = m_globalAutoUpdateInitialInterval;
131   m_globalAutoUpdateEnabled = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::AutoUpdateEnabled)).toBool();
132   m_globalAutoUpdateOnlyUnfocused = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::AutoUpdateOnlyUnfocused)).toBool();
133 
134   // Start global auto-update timer if it is not running yet.
135   // NOTE: The timer must run even if global auto-update
136   // is not enabled because user can still enable auto-update
137   // for individual feeds.
138   if (!m_autoUpdateTimer->isActive()) {
139     m_autoUpdateTimer->setInterval(AUTO_UPDATE_INTERVAL);
140     m_autoUpdateTimer->start();
141     qDebugNN << LOGSEC_CORE << "Auto-download timer started with interval "
142              << m_autoUpdateTimer->interval()
143              << " ms.";
144   }
145   else {
146     qDebugNN << LOGSEC_CORE << "Auto-download timer is already running.";
147   }
148 }
149 
autoUpdateEnabled() const150 bool FeedReader::autoUpdateEnabled() const {
151   return m_globalAutoUpdateEnabled;
152 }
153 
autoUpdateRemainingInterval() const154 int FeedReader::autoUpdateRemainingInterval() const {
155   return m_globalAutoUpdateRemainingInterval;
156 }
157 
autoUpdateInitialInterval() const158 int FeedReader::autoUpdateInitialInterval() const {
159   return m_globalAutoUpdateInitialInterval;
160 }
161 
loadSavedMessageFilters()162 void FeedReader::loadSavedMessageFilters() {
163   // Load all message filters from database.
164   // All plugin services will hook active filters to
165   // all feeds.
166   m_messageFilters = DatabaseQueries::getMessageFilters(qApp->database()->driver()->connection(metaObject()->className()));
167 
168   for (auto* filter : qAsConst(m_messageFilters)) {
169     filter->setParent(this);
170   }
171 }
172 
addMessageFilter(const QString & title,const QString & script)173 MessageFilter* FeedReader::addMessageFilter(const QString& title, const QString& script) {
174   auto* fltr = DatabaseQueries::addMessageFilter(qApp->database()->driver()->connection(metaObject()->className()), title, script);
175 
176   m_messageFilters.append(fltr);
177   return fltr;
178 }
179 
removeMessageFilter(MessageFilter * filter)180 void FeedReader::removeMessageFilter(MessageFilter* filter) {
181   m_messageFilters.removeAll(filter);
182 
183   // Now, remove all references from all feeds.
184   auto all_feeds = m_feedsModel->feedsForIndex();
185 
186   for (auto* feed : all_feeds) {
187     feed->removeMessageFilter(filter);
188   }
189 
190   // Remove from DB.
191   DatabaseQueries::removeMessageFilterAssignments(qApp->database()->driver()->connection(metaObject()->className()), filter->id());
192   DatabaseQueries::removeMessageFilter(qApp->database()->driver()->connection(metaObject()->className()), filter->id());
193 
194   // Free from memory as last step.
195   filter->deleteLater();
196 }
197 
updateMessageFilter(MessageFilter * filter)198 void FeedReader::updateMessageFilter(MessageFilter* filter) {
199   DatabaseQueries::updateMessageFilter(qApp->database()->driver()->connection(metaObject()->className()), filter);
200 }
201 
assignMessageFilterToFeed(Feed * feed,MessageFilter * filter)202 void FeedReader::assignMessageFilterToFeed(Feed* feed, MessageFilter* filter) {
203   feed->appendMessageFilter(filter);
204   DatabaseQueries::assignMessageFilterToFeed(qApp->database()->driver()->connection(metaObject()->className()),
205                                              feed->customId(),
206                                              filter->id(),
207                                              feed->getParentServiceRoot()->accountId());
208 }
209 
removeMessageFilterToFeedAssignment(Feed * feed,MessageFilter * filter)210 void FeedReader::removeMessageFilterToFeedAssignment(Feed* feed, MessageFilter* filter) {
211   feed->removeMessageFilter(filter);
212   DatabaseQueries::removeMessageFilterFromFeed(qApp->database()->driver()->connection(metaObject()->className()),
213                                                feed->customId(),
214                                                filter->id(),
215                                                feed->getParentServiceRoot()->accountId());
216 }
217 
updateAllFeeds()218 void FeedReader::updateAllFeeds() {
219   updateFeeds(m_feedsModel->rootItem()->getSubTreeFeeds());
220 }
221 
updateManuallyIntervaledFeeds()222 void FeedReader::updateManuallyIntervaledFeeds() {
223   updateFeeds(m_feedsModel->rootItem()->getSubTreeAutoFetchingWithManualIntervalsFeeds());
224 }
225 
stopRunningFeedUpdate()226 void FeedReader::stopRunningFeedUpdate() {
227   if (m_feedDownloader != nullptr) {
228     m_feedDownloader->stopRunningUpdate();
229   }
230 }
231 
isFeedUpdateRunning() const232 bool FeedReader::isFeedUpdateRunning() const {
233   return m_feedDownloader != nullptr && m_feedDownloader->isUpdateRunning();
234 }
235 
feedDownloader() const236 FeedDownloader* FeedReader::feedDownloader() const {
237   return m_feedDownloader;
238 }
239 
feedsModel() const240 FeedsModel* FeedReader::feedsModel() const {
241   return m_feedsModel;
242 }
243 
messagesModel() const244 MessagesModel* FeedReader::messagesModel() const {
245   return m_messagesModel;
246 }
247 
executeNextAutoUpdate()248 void FeedReader::executeNextAutoUpdate() {
249   bool disable_update_with_window = qApp->mainFormWidget()->isActiveWindow() && m_globalAutoUpdateOnlyUnfocused;
250   auto roots = qApp->feedReader()->feedsModel()->serviceRoots();
251   std::list<CacheForServiceRoot*> full_caches = boolinq::from(roots)
252                                                 .select([](ServiceRoot* root) -> CacheForServiceRoot* {
253     auto* cache = root->toCache();
254 
255     if (cache != nullptr) {
256       return cache;
257     }
258     else {
259       return nullptr;
260     }
261   })
262                                                 .where([](CacheForServiceRoot* cache) {
263     return cache != nullptr && !cache->isEmpty();
264   }).toStdList();
265 
266   // Skip this round of auto-updating, but only if user disabled it when main window is active
267   // and there are no caches to synchronize.
268   if (disable_update_with_window && full_caches.empty()) {
269     qDebugNN << LOGSEC_CORE
270              << "Delaying scheduled feed auto-download for one minute since window "
271              << "is focused and updates while focused are disabled by the "
272              << "user and all account caches are empty.";
273 
274     // Cannot update, quit.
275     return;
276   }
277 
278   if (!qApp->feedUpdateLock()->tryLock()) {
279     qDebugNN << LOGSEC_CORE
280              << "Delaying scheduled feed auto-downloads and message state synchronization for "
281              << "one minute due to another running update.";
282 
283     // Cannot update, quit.
284     return;
285   }
286 
287   // If global auto-update is enabled and its interval counter reached zero,
288   // then we need to restore it.
289   if (m_globalAutoUpdateEnabled && --m_globalAutoUpdateRemainingInterval < 0) {
290     // We should start next auto-update interval.
291     m_globalAutoUpdateRemainingInterval = m_globalAutoUpdateInitialInterval - 1;
292   }
293 
294   qDebugNN << LOGSEC_CORE
295            << "Starting auto-download event, remaining "
296            << m_globalAutoUpdateRemainingInterval << " minutes out of "
297            << m_globalAutoUpdateInitialInterval << " total minutes to next global feed update.";
298 
299   qApp->feedUpdateLock()->unlock();
300 
301   // Resynchronize caches.
302   if (!full_caches.empty()) {
303     QList<CacheForServiceRoot*> caches = FROM_STD_LIST(QList<CacheForServiceRoot*>, full_caches);
304 
305     synchronizeMessageData(caches);
306   }
307 
308   // Pass needed interval data and lets the model decide which feeds
309   // should be updated in this pass.
310   QList<Feed*> feeds_for_update = m_feedsModel->feedsForScheduledUpdate(m_globalAutoUpdateEnabled &&
311                                                                         m_globalAutoUpdateRemainingInterval == 0);
312 
313   if (!feeds_for_update.isEmpty()) {
314     // Request update for given feeds.
315     updateFeeds(feeds_for_update);
316 
317     // NOTE: OSD/bubble informing about performing of scheduled update can be shown now.
318     qApp->showGuiMessage(Notification::Event::ArticlesFetchingStarted,
319                          tr("Starting auto-download of some feeds' articles"),
320                          tr("I will auto-download new articles for %n feed(s).", nullptr, feeds_for_update.size()),
321                          QSystemTrayIcon::MessageIcon::Information);
322   }
323 }
324 
messageFilters() const325 QList<MessageFilter*> FeedReader::messageFilters() const {
326   return m_messageFilters;
327 }
328 
quit()329 void FeedReader::quit() {
330   if (m_autoUpdateTimer->isActive()) {
331     m_autoUpdateTimer->stop();
332   }
333 
334   // Stop running updates.
335   if (m_feedDownloader != nullptr) {
336     m_feedDownloader->stopRunningUpdate();
337 
338     if (m_feedDownloader->isUpdateRunning() || m_feedDownloader->isCacheSynchronizationRunning()) {
339       QEventLoop loop(this);
340 
341       connect(m_feedDownloader, &FeedDownloader::cachesSynchronized, &loop, &QEventLoop::quit);
342       connect(m_feedDownloader, &FeedDownloader::updateFinished, &loop, &QEventLoop::quit);
343       loop.exec();
344     }
345 
346     // Both thread and downloader are auto-deleted when worker thread exits.
347     m_feedDownloaderThread->quit();
348   }
349 
350   if (qApp->settings()->value(GROUP(Messages), SETTING(Messages::ClearReadOnExit)).toBool()) {
351     m_feedsModel->markItemCleared(m_feedsModel->rootItem(), true);
352   }
353 
354   m_feedsModel->stopServiceAccounts();
355 }
356 
messagesProxyModel() const357 MessagesProxyModel* FeedReader::messagesProxyModel() const {
358   return m_messagesProxyModel;
359 }
360 
feedsProxyModel() const361 FeedsProxyModel* FeedReader::feedsProxyModel() const {
362   return m_feedsProxyModel;
363 }
364