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