1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2018 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 **************************************************************************/
19
20 #include "FeedsManager.h"
21 #include "Application.h"
22 #include "BookmarksManager.h"
23 #include "Console.h"
24 #include "FeedParser.h"
25 #include "Job.h"
26 #include "LongTermTimer.h"
27 #include "NotificationsManager.h"
28 #include "SessionsManager.h"
29 #include "Utils.h"
30
31 #include <QtCore/QFile>
32 #include <QtCore/QJsonArray>
33 #include <QtCore/QJsonDocument>
34 #include <QtCore/QJsonObject>
35 #include <QtCore/QSaveFile>
36
37 namespace Otter
38 {
39
Feed(const QString & title,const QUrl & url,const QIcon & icon,int updateInterval,QObject * parent)40 Feed::Feed(const QString &title, const QUrl &url, const QIcon &icon, int updateInterval, QObject *parent) : QObject(parent),
41 m_updateTimer(nullptr),
42 m_parser(nullptr),
43 m_title(title),
44 m_url(url),
45 m_icon(icon),
46 m_error(NoError),
47 m_updateInterval(0),
48 m_isUpdating(false)
49 {
50 setUpdateInterval(updateInterval);
51 }
52
markEntryAsRead(const QString & identifier)53 void Feed::markEntryAsRead(const QString &identifier)
54 {
55 for (int i = 0; i < m_entries.count(); ++i)
56 {
57 if (m_entries.at(i).identifier == identifier)
58 {
59 m_entries[i].lastReadTime = QDateTime::currentDateTimeUtc();
60
61 emit feedModified(this);
62
63 break;
64 }
65 }
66 }
67
markEntryAsRemoved(const QString & identifier)68 void Feed::markEntryAsRemoved(const QString &identifier)
69 {
70 if (!m_removedEntries.contains(identifier))
71 {
72 for (int i = 0; i < m_entries.count(); ++i)
73 {
74 if (m_entries.at(i).identifier == identifier)
75 {
76 m_entries.removeAt(i);
77
78 m_removedEntries.append(identifier);
79
80 emit feedModified(this);
81
82 break;
83 }
84 }
85 }
86 }
87
setTitle(const QString & title)88 void Feed::setTitle(const QString &title)
89 {
90 if (title != m_title)
91 {
92 m_title = title;
93
94 emit feedModified(this);
95 }
96 }
97
setDescription(const QString & description)98 void Feed::setDescription(const QString &description)
99 {
100 if (description != m_description)
101 {
102 m_description = description;
103
104 emit feedModified(this);
105 }
106 }
107
setUrl(const QUrl & url)108 void Feed::setUrl(const QUrl &url)
109 {
110 if (url != m_url)
111 {
112 m_url = url;
113
114 update();
115
116 emit feedModified(this);
117 }
118 }
119
setIcon(const QIcon & icon)120 void Feed::setIcon(const QIcon &icon)
121 {
122 m_icon = icon;
123
124 emit feedModified(this);
125 }
126
setLastUpdateTime(const QDateTime & time)127 void Feed::setLastUpdateTime(const QDateTime &time)
128 {
129 m_lastUpdateTime = time;
130 }
131
setLastSynchronizationTime(const QDateTime & time)132 void Feed::setLastSynchronizationTime(const QDateTime &time)
133 {
134 m_lastSynchronizationTime = time;
135 }
136
setCategories(const QMap<QString,QString> & categories)137 void Feed::setCategories(const QMap<QString, QString> &categories)
138 {
139 m_categories = categories;
140 }
141
setRemovedEntries(const QStringList & removedEntries)142 void Feed::setRemovedEntries(const QStringList &removedEntries)
143 {
144 m_removedEntries = removedEntries;
145 }
146
setEntries(const QVector<Feed::Entry> & entries)147 void Feed::setEntries(const QVector<Feed::Entry> &entries)
148 {
149 m_entries = entries;
150 }
151
setUpdateInterval(int interval)152 void Feed::setUpdateInterval(int interval)
153 {
154 if (interval != m_updateInterval)
155 {
156 m_updateInterval = interval;
157
158 if (interval <= 0 && m_updateTimer)
159 {
160 m_updateTimer->deleteLater();
161 m_updateTimer = nullptr;
162 }
163 else
164 {
165 if (!m_updateTimer)
166 {
167 m_updateTimer = new LongTermTimer(this);
168
169 connect(m_updateTimer, &LongTermTimer::timeout, this, &Feed::update);
170 }
171
172 m_updateTimer->start(static_cast<quint64>(interval) * 60000);
173 }
174
175 emit feedModified(this);
176 }
177 }
178
update()179 void Feed::update()
180 {
181 if (m_parser)
182 {
183 return;
184 }
185
186 m_error = NoError;
187 m_isUpdating = true;
188
189 emit feedModified(this);
190
191 DataFetchJob *dataJob(new DataFetchJob(m_url, this));
192
193 connect(dataJob, &DataFetchJob::jobFinished, this, [=](bool isFetchSuccess)
194 {
195 if (isFetchSuccess)
196 {
197 m_parser = FeedParser::createParser(this, dataJob);
198
199 if (m_parser)
200 {
201 m_parser->moveToThread(&m_parserThread);
202
203 connect(m_parser, &FeedParser::parsingFinished, [&](bool isParsingSuccess)
204 {
205 const FeedParser::FeedInformation information(m_parser->getInformation());
206
207 if (!isParsingSuccess)
208 {
209 m_error = ParseError;
210 }
211
212 if (m_icon.isNull() && information.icon.isValid())
213 {
214 IconFetchJob *iconJob(new IconFetchJob(information.icon, this));
215
216 connect(iconJob, &IconFetchJob::jobFinished, this, [=]()
217 {
218 setIcon(iconJob->getIcon());
219 });
220
221 iconJob->start();
222 }
223
224 if (m_title.isEmpty())
225 {
226 m_title = information.title;
227 }
228
229 if (m_description.isEmpty())
230 {
231 m_description = information.description;
232 }
233
234 if (!information.entries.isEmpty())
235 {
236 QStringList existingRemovedEntries;
237 int amount(0);
238
239 for (int i = (information.entries.count() - 1); i >= 0; --i)
240 {
241 Feed::Entry entry(information.entries.at(i));
242
243 if (m_removedEntries.contains(entry.identifier))
244 {
245 existingRemovedEntries.append(entry.identifier);
246 }
247 else
248 {
249 bool hasEntry(false);
250
251 for (int j = 0; j < m_entries.count(); ++j)
252 {
253 const Feed::Entry existingEntry(m_entries.at(j));
254
255 if (existingEntry.identifier == entry.identifier)
256 {
257 if ((entry.publicationTime.isValid() && existingEntry.publicationTime != entry.publicationTime) || (entry.updateTime.isValid() && existingEntry.updateTime != entry.updateTime))
258 {
259 ++amount;
260 }
261
262 entry.lastReadTime = existingEntry.lastReadTime;
263 entry.publicationTime = normalizeTime(entry.publicationTime);
264
265 if (entry.updateTime.isValid())
266 {
267 entry.updateTime = normalizeTime(entry.updateTime);
268 }
269
270 m_entries[j] = entry;
271
272 hasEntry = true;
273
274 break;
275 }
276 }
277
278 if (!hasEntry)
279 {
280 ++amount;
281
282 entry.publicationTime = normalizeTime(entry.publicationTime);
283 entry.updateTime = normalizeTime(entry.updateTime);
284
285 m_entries.prepend(entry);
286 }
287 }
288 }
289
290 m_removedEntries = existingRemovedEntries;
291
292 if (amount > 0)
293 {
294 connect(NotificationsManager::createNotification(NotificationsManager::FeedUpdatedEvent, tr("Feed updated:\n%1").arg(getTitle()), Notification::InformationLevel, this), &Notification::clicked, [&]()
295 {
296 Application::getInstance()->triggerAction(ActionsManager::OpenUrlAction, {{QLatin1String("url"), QUrl(QLatin1String("view-feed:") + getUrl().toDisplayString())}});
297 });
298 }
299
300 emit entriesModified(this);
301 }
302
303 m_mimeType = information.mimeType;
304 m_lastSynchronizationTime = QDateTime::currentDateTimeUtc();
305 m_lastUpdateTime = information.lastUpdateTime;
306 m_categories = information.categories;
307
308 m_parserThread.exit();
309
310 m_parser->deleteLater();
311 m_parser = nullptr;
312
313 m_isUpdating = false;
314
315 emit feedModified(this);
316 });
317
318 m_parserThread.start();
319
320 m_parser->parse(dataJob);
321 }
322 else
323 {
324 m_error = ParseError;
325 m_isUpdating = false;
326
327 Console::addMessage(tr("Failed to parse feed: invalid feed type"), Console::NetworkCategory, Console::ErrorLevel, m_url.toDisplayString());
328
329 emit feedModified(this);
330 }
331 }
332 else
333 {
334 m_error = DownloadError;
335 m_isUpdating = false;
336
337 Console::addMessage(tr("Failed to download feed"), Console::NetworkCategory, Console::ErrorLevel, m_url.toDisplayString());
338
339 emit feedModified(this);
340 }
341 });
342
343 dataJob->start();
344 }
345
getTitle() const346 QString Feed::getTitle() const
347 {
348 return m_title;
349 }
350
getDescription() const351 QString Feed::getDescription() const
352 {
353 return m_description;
354 }
355
getUrl() const356 QUrl Feed::getUrl() const
357 {
358 return m_url;
359 }
360
getIcon() const361 QIcon Feed::getIcon() const
362 {
363 return m_icon;
364 }
365
getLastUpdateTime() const366 QDateTime Feed::getLastUpdateTime() const
367 {
368 return m_lastUpdateTime;
369 }
370
getLastSynchronizationTime() const371 QDateTime Feed::getLastSynchronizationTime() const
372 {
373 return m_lastSynchronizationTime;
374 }
375
normalizeTime(const QDateTime & time) const376 QDateTime Feed::normalizeTime(const QDateTime &time) const
377 {
378 return ((time.isValid() && time < QDateTime::currentDateTimeUtc()) ? time : QDateTime::currentDateTimeUtc());
379 }
380
getMimeType() const381 QMimeType Feed::getMimeType() const
382 {
383 return m_mimeType;
384 }
385
getCategories() const386 QMap<QString, QString> Feed::getCategories() const
387 {
388 return m_categories;
389 }
390
getRemovedEntries() const391 QStringList Feed::getRemovedEntries() const
392 {
393 return m_removedEntries;
394 }
395
getEntries(const QStringList & categories) const396 QVector<Feed::Entry> Feed::getEntries(const QStringList &categories) const
397 {
398 if (!categories.isEmpty())
399 {
400 QVector<Entry> entries;
401 entries.reserve(entries.count() / 2);
402
403 for (int i = 0; i < m_entries.count(); ++i)
404 {
405 const Feed::Entry entry(m_entries.at(i));
406
407 if (!entry.categories.isEmpty())
408 {
409 for (int j = 0; j < categories.count(); ++j)
410 {
411 if (entry.categories.contains(categories.at(j)))
412 {
413 entries.append(entry);
414
415 break;
416 }
417 }
418 }
419 }
420
421 entries.squeeze();
422
423 return entries;
424 }
425
426 return m_entries;
427 }
428
getError() const429 Feed::FeedError Feed::getError() const
430 {
431 return m_error;
432 }
433
getUpdateInterval() const434 int Feed::getUpdateInterval() const
435 {
436 return m_updateInterval;
437 }
438
isUpdating() const439 bool Feed::isUpdating() const
440 {
441 return m_isUpdating;
442 }
443
444 FeedsManager* FeedsManager::m_instance(nullptr);
445 FeedsModel* FeedsManager::m_model(nullptr);
446 QVector<Feed*> FeedsManager::m_feeds;
447 bool FeedsManager::m_isInitialized(false);
448
FeedsManager(QObject * parent)449 FeedsManager::FeedsManager(QObject *parent) : QObject(parent),
450 m_saveTimer(0)
451 {
452 }
453
timerEvent(QTimerEvent * event)454 void FeedsManager::timerEvent(QTimerEvent *event)
455 {
456 if (event->timerId() == m_saveTimer)
457 {
458 killTimer(m_saveTimer);
459
460 m_saveTimer = 0;
461
462 if (m_model)
463 {
464 m_model->save(SessionsManager::getWritableDataPath(QLatin1String("feeds.opml")));
465 }
466
467 if (SessionsManager::isReadOnly())
468 {
469 return;
470 }
471
472 QSaveFile file(SessionsManager::getWritableDataPath(QLatin1String("feeds.json")));
473
474 if (!file.open(QIODevice::WriteOnly))
475 {
476 return;
477 }
478
479 QJsonArray feedsArray;
480
481 for (int i = 0; i < m_feeds.count(); ++i)
482 {
483 const Feed *feed(m_feeds.at(i));
484
485 if (!FeedsManager::getModel()->hasFeed(feed->getUrl()) && !BookmarksManager::getModel()->hasFeed(feed->getUrl()))
486 {
487 continue;
488 }
489
490 const QMap<QString, QString> categories(feed->getCategories());
491 QJsonObject feedObject({{QLatin1String("title"), feed->getTitle()}, {QLatin1String("url"), feed->getUrl().toString()}, {QLatin1String("updateInterval"), QString::number(feed->getUpdateInterval())}, {QLatin1String("lastSynchronizationTime"), feed->getLastUpdateTime().toString(Qt::ISODate)}, {QLatin1String("lastUpdateTime"), feed->getLastSynchronizationTime().toString(Qt::ISODate)}});
492
493 if (!feed->getDescription().isEmpty())
494 {
495 feedObject.insert(QLatin1String("description"), feed->getDescription());
496 }
497
498 if (!feed->getIcon().isNull())
499 {
500 feedObject.insert(QLatin1String("icon"), Utils::savePixmapAsDataUri(feed->getIcon().pixmap(feed->getIcon().availableSizes().value(0, QSize(16, 16)))));
501 }
502
503 if (!categories.isEmpty())
504 {
505 QMap<QString, QString>::const_iterator iterator;
506 QJsonObject categoriesObject;
507
508 for (iterator = categories.begin(); iterator != categories.end(); ++iterator)
509 {
510 categoriesObject.insert(iterator.key(), iterator.value());
511 }
512
513 feedObject.insert(QLatin1String("categories"), categoriesObject);
514 }
515
516 if (!feed->getRemovedEntries().isEmpty())
517 {
518 feedObject.insert(QLatin1String("removedEntries"), QJsonArray::fromStringList(feed->getRemovedEntries()));
519 }
520
521 const QVector<Feed::Entry> entries(feed->getEntries());
522 QJsonArray entriesArray;
523
524 for (int j = 0; j < entries.count(); ++j)
525 {
526 const Feed::Entry entry(entries.at(j));
527 QJsonObject entryObject({{QLatin1String("identifier"), entry.identifier}, {QLatin1String("title"), entry.title}});
528
529 if (!entry.summary.isEmpty())
530 {
531 entryObject.insert(QLatin1String("summary"), entry.summary);
532 }
533
534 if (!entry.content.isEmpty())
535 {
536 entryObject.insert(QLatin1String("content"), entry.content);
537 }
538
539 if (!entry.author.isEmpty())
540 {
541 entryObject.insert(QLatin1String("author"), entry.author);
542 }
543
544 if (!entry.email.isEmpty())
545 {
546 entryObject.insert(QLatin1String("email"), entry.email);
547 }
548
549 if (!entry.url.isEmpty())
550 {
551 entryObject.insert(QLatin1String("url"), entry.url.toString());
552 }
553
554 if (entry.lastReadTime.isValid())
555 {
556 entryObject.insert(QLatin1String("lastReadTime"), entry.lastReadTime.toString(Qt::ISODate));
557 }
558
559 if (entry.publicationTime.isValid())
560 {
561 entryObject.insert(QLatin1String("publicationTime"), entry.publicationTime.toString(Qt::ISODate));
562 }
563
564 if (entry.updateTime.isValid())
565 {
566 entryObject.insert(QLatin1String("updateTime"), entry.updateTime.toString(Qt::ISODate));
567 }
568
569 if (!entry.categories.isEmpty())
570 {
571 entryObject.insert(QLatin1String("categories"), QJsonArray::fromStringList(entry.categories));
572 }
573
574 entriesArray.append(entryObject);
575 }
576
577 feedObject.insert(QLatin1String("entries"), entriesArray);
578
579 feedsArray.append(feedObject);
580 }
581
582 QJsonDocument document;
583 document.setArray(feedsArray);
584
585 file.write(document.toJson());
586 file.commit();
587 }
588 }
589
createInstance()590 void FeedsManager::createInstance()
591 {
592 if (!m_instance)
593 {
594 m_instance = new FeedsManager(QCoreApplication::instance());
595 }
596 }
597
ensureInitialized()598 void FeedsManager::ensureInitialized()
599 {
600 if (m_isInitialized)
601 {
602 return;
603 }
604
605 m_isInitialized = true;
606
607 QFile file(SessionsManager::getWritableDataPath(QLatin1String("feeds.json")));
608
609 if (file.open(QIODevice::ReadOnly))
610 {
611 const QJsonArray feedsArray(QJsonDocument::fromJson(file.readAll()).array());
612
613 for (int i = 0; i < feedsArray.count(); ++i)
614 {
615 const QJsonObject feedObject(feedsArray.at(i).toObject());
616 Feed *feed(createFeed(QUrl(feedObject.value(QLatin1String("url")).toString()), feedObject.value(QLatin1String("title")).toString(), Utils::loadPixmapFromDataUri(feedObject.value(QLatin1String("icon")).toString()), feedObject.value(QLatin1String("updateInterval")).toInt()));
617 feed->setDescription(feedObject.value(QLatin1String("description")).toString());
618 feed->setLastUpdateTime(QDateTime::fromString(feedObject.value(QLatin1String("lastUpdateTime")).toString(), Qt::ISODate));
619 feed->setLastSynchronizationTime(QDateTime::fromString(feedObject.value(QLatin1String("lastSynchronizationTime")).toString(), Qt::ISODate));
620 feed->setRemovedEntries(feedObject.value(QLatin1String("removedEntries")).toVariant().toStringList());
621
622 if (feedObject.contains(QLatin1String("categories")))
623 {
624 QMap<QString, QString> categories;
625 const QVariantMap rawCategories(feedObject.value(QLatin1String("categories")).toVariant().toMap());
626 QVariantMap::const_iterator iterator;
627
628 for (iterator = rawCategories.begin(); iterator != rawCategories.end(); ++iterator)
629 {
630 categories[iterator.key()] = iterator.value().toString();
631 }
632
633 feed->setCategories(categories);
634 }
635
636 const QJsonArray entriesArray(feedObject.value(QLatin1String("entries")).toArray());
637 QVector<Feed::Entry> entries;
638 entries.reserve(entriesArray.count());
639
640 for (int j = 0; j < entriesArray.count(); ++j)
641 {
642 const QJsonObject entryObject(entriesArray.at(j).toObject());
643 Feed::Entry entry;
644 entry.identifier = entryObject.value(QLatin1String("identifier")).toString();
645 entry.title = entryObject.value(QLatin1String("title")).toString();
646 entry.summary = entryObject.value(QLatin1String("summary")).toString();
647 entry.content = entryObject.value(QLatin1String("content")).toString();
648 entry.author = entryObject.value(QLatin1String("author")).toString();
649 entry.email = entryObject.value(QLatin1String("email")).toString();
650 entry.url = entryObject.value(QLatin1String("url")).toString();
651 entry.lastReadTime = QDateTime::fromString(entryObject.value(QLatin1String("lastReadTime")).toString(), Qt::ISODate);
652 entry.publicationTime = QDateTime::fromString(entryObject.value(QLatin1String("publicationTime")).toString(), Qt::ISODate);
653 entry.updateTime = QDateTime::fromString(entryObject.value(QLatin1String("updateTime")).toString(), Qt::ISODate);
654 entry.categories = entryObject.value(QLatin1String("categories")).toVariant().toStringList();
655
656 entries.append(entry);
657 }
658
659 feed->setEntries(entries);
660 }
661 }
662
663 if (!m_model)
664 {
665 m_model = new FeedsModel(SessionsManager::getWritableDataPath(QLatin1String("feeds.opml")), m_instance);
666
667 connect(m_model, &FeedsModel::modelModified, m_instance, &FeedsManager::scheduleSave);
668 }
669 }
670
scheduleSave()671 void FeedsManager::scheduleSave()
672 {
673 if (m_saveTimer == 0)
674 {
675 m_saveTimer = startTimer(1000);
676 }
677 }
678
handleFeedModified(Feed * feed)679 void FeedsManager::handleFeedModified(Feed *feed)
680 {
681 if (feed)
682 {
683 emit feedModified(feed->getUrl());
684 }
685
686 scheduleSave();
687 }
688
getInstance()689 FeedsManager* FeedsManager::getInstance()
690 {
691 return m_instance;
692 }
693
getModel()694 FeedsModel* FeedsManager::getModel()
695 {
696 ensureInitialized();
697
698 return m_model;
699 }
700
createFeed(const QUrl & url,const QString & title,const QIcon & icon,int updateInterval)701 Feed* FeedsManager::createFeed(const QUrl &url, const QString &title, const QIcon &icon, int updateInterval)
702 {
703 ensureInitialized();
704
705 Feed *feed(getFeed(url));
706
707 if (feed)
708 {
709 return feed;
710 }
711
712 feed = new Feed(title, url, icon, updateInterval, m_instance);
713
714 m_feeds.append(feed);
715
716 connect(feed, &Feed::feedModified, m_instance, &FeedsManager::handleFeedModified);
717
718 return feed;
719 }
720
getFeed(const QUrl & url)721 Feed* FeedsManager::getFeed(const QUrl &url)
722 {
723 ensureInitialized();
724
725 const QUrl normalizedUrl(Utils::normalizeUrl(url));
726
727 for (int i = 0; i < m_feeds.count(); ++i)
728 {
729 Feed *feed(m_feeds.at(i));
730
731 if (feed->getUrl() == url || feed->getUrl() == normalizedUrl)
732 {
733 return feed;
734 }
735 }
736
737 return nullptr;
738 }
739
getFeeds()740 QVector<Feed*> FeedsManager::getFeeds()
741 {
742 ensureInitialized();
743
744 return m_feeds;
745 }
746
747 }
748