1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "services/abstract/serviceroot.h"
4 
5 #include "3rd-party/boolinq/boolinq.h"
6 #include "core/feedsmodel.h"
7 #include "core/messagesmodel.h"
8 #include "database/databasequeries.h"
9 #include "exceptions/applicationexception.h"
10 #include "miscellaneous/application.h"
11 #include "miscellaneous/iconfactory.h"
12 #include "miscellaneous/textfactory.h"
13 #include "services/abstract/cacheforserviceroot.h"
14 #include "services/abstract/category.h"
15 #include "services/abstract/feed.h"
16 #include "services/abstract/importantnode.h"
17 #include "services/abstract/labelsnode.h"
18 #include "services/abstract/recyclebin.h"
19 #include "services/abstract/unreadnode.h"
20 
21 #include <QThread>
22 
ServiceRoot(RootItem * parent)23 ServiceRoot::ServiceRoot(RootItem* parent)
24   : RootItem(parent), m_recycleBin(new RecycleBin(this)), m_importantNode(new ImportantNode(this)),
25   m_labelsNode(new LabelsNode(this)), m_unreadNode(new UnreadNode(this)),
26   m_accountId(NO_PARENT_CATEGORY), m_networkProxy(QNetworkProxy()) {
27   setKind(RootItem::Kind::ServiceRoot);
28   appendCommonNodes();
29 }
30 
31 ServiceRoot::~ServiceRoot() = default;
32 
deleteViaGui()33 bool ServiceRoot::deleteViaGui() {
34   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
35 
36   if (DatabaseQueries::deleteAccount(database, accountId())) {
37     stop();
38     requestItemRemoval(this);
39     return true;
40   }
41   else {
42     return false;
43   }
44 }
45 
markAsReadUnread(RootItem::ReadStatus status)46 bool ServiceRoot::markAsReadUnread(RootItem::ReadStatus status) {
47   auto* cache = dynamic_cast<CacheForServiceRoot*>(this);
48 
49   if (cache != nullptr) {
50     cache->addMessageStatesToCache(customIDSOfMessagesForItem(this), status);
51   }
52 
53   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
54 
55   if (DatabaseQueries::markAccountReadUnread(database, accountId(), status)) {
56     updateCounts(false);
57     itemChanged(getSubTree());
58     requestReloadMessageList(status == RootItem::ReadStatus::Read);
59     return true;
60   }
61   else {
62     return false;
63   }
64 }
65 
addItemMenu()66 QList<QAction*> ServiceRoot::addItemMenu() {
67   return QList<QAction*>();
68 }
69 
recycleBin() const70 RecycleBin* ServiceRoot::recycleBin() const {
71   return m_recycleBin;
72 }
73 
downloadAttachmentOnMyOwn(const QUrl & url) const74 bool ServiceRoot::downloadAttachmentOnMyOwn(const QUrl& url) const {
75   Q_UNUSED(url)
76   return false;
77 }
78 
contextMenuFeedsList()79 QList<QAction*> ServiceRoot::contextMenuFeedsList() {
80   return serviceMenu();
81 }
82 
contextMenuMessagesList(const QList<Message> & messages)83 QList<QAction*> ServiceRoot::contextMenuMessagesList(const QList<Message>& messages) {
84   Q_UNUSED(messages)
85   return {};
86 }
87 
serviceMenu()88 QList<QAction*> ServiceRoot::serviceMenu() {
89   if (m_serviceMenu.isEmpty()) {
90     if (isSyncable()) {
91       auto* act_sync_tree = new QAction(qApp->icons()->fromTheme(QSL("view-refresh")), tr("Synchronize folders && other items"), this);
92 
93       connect(act_sync_tree, &QAction::triggered, this, &ServiceRoot::syncIn);
94       m_serviceMenu.append(act_sync_tree);
95 
96       auto* cache = toCache();
97 
98       if (cache != nullptr) {
99         auto* act_sync_cache = new QAction(qApp->icons()->fromTheme(QSL("view-refresh")), tr("Synchronize article cache"), this);
100 
101         connect(act_sync_cache, &QAction::triggered, this, [cache]() {
102           cache->saveAllCachedData(false);
103         });
104 
105         m_serviceMenu.append(act_sync_cache);
106       }
107     }
108   }
109 
110   return m_serviceMenu;
111 }
112 
isSyncable() const113 bool ServiceRoot::isSyncable() const {
114   return false;
115 }
116 
start(bool freshly_activated)117 void ServiceRoot::start(bool freshly_activated) {
118   Q_UNUSED(freshly_activated)
119 }
120 
stop()121 void ServiceRoot::stop() {}
122 
updateCounts(bool including_total_count)123 void ServiceRoot::updateCounts(bool including_total_count) {
124   QList<Feed*> feeds;
125   auto str = getSubTree();
126 
127   for (RootItem* child : qAsConst(str)) {
128     if (child->kind() == RootItem::Kind::Feed) {
129       feeds.append(child->toFeed());
130     }
131     else if (child->kind() != RootItem::Kind::Labels &&
132              child->kind() != RootItem::Kind::Category &&
133              child->kind() != RootItem::Kind::ServiceRoot) {
134       child->updateCounts(including_total_count);
135     }
136   }
137 
138   if (feeds.isEmpty()) {
139     return;
140   }
141 
142   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
143   bool ok;
144   QMap<QString, QPair<int, int>> counts = DatabaseQueries::getMessageCountsForAccount(database, accountId(), including_total_count, &ok);
145 
146   if (ok) {
147     for (Feed* feed : feeds) {
148       if (counts.contains(feed->customId())) {
149         feed->setCountOfUnreadMessages(counts.value(feed->customId()).first);
150 
151         if (including_total_count) {
152           feed->setCountOfAllMessages(counts.value(feed->customId()).second);
153         }
154       }
155       else {
156         feed->setCountOfUnreadMessages(0);
157 
158         if (including_total_count) {
159           feed->setCountOfAllMessages(0);
160         }
161       }
162     }
163   }
164 }
165 
canBeDeleted() const166 bool ServiceRoot::canBeDeleted() const {
167   return true;
168 }
169 
completelyRemoveAllData()170 void ServiceRoot::completelyRemoveAllData() {
171   // Purge old data from SQL and clean all model items.
172   cleanAllItemsFromModel(true);
173   removeOldAccountFromDatabase(true, true);
174   updateCounts(true);
175   itemChanged({ this });
176   requestReloadMessageList(true);
177 }
178 
feedIconForMessage(const QString & feed_custom_id) const179 QIcon ServiceRoot::feedIconForMessage(const QString& feed_custom_id) const {
180   QString low_id = feed_custom_id.toLower();
181   RootItem* found_item = getItemFromSubTree([low_id](const RootItem* it) {
182     return it->kind() == RootItem::Kind::Feed && it->customId().toLower() == low_id;
183   });
184 
185   if (found_item != nullptr) {
186     return found_item->icon();
187   }
188   else {
189     return QIcon();
190   }
191 }
192 
removeOldAccountFromDatabase(bool delete_messages_too,bool delete_labels_too)193 void ServiceRoot::removeOldAccountFromDatabase(bool delete_messages_too, bool delete_labels_too) {
194   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
195 
196   DatabaseQueries::deleteAccountData(database,
197                                      accountId(),
198                                      delete_messages_too,
199                                      delete_labels_too);
200 }
201 
cleanAllItemsFromModel(bool clean_labels_too)202 void ServiceRoot::cleanAllItemsFromModel(bool clean_labels_too) {
203   auto chi = childItems();
204 
205   for (RootItem* top_level_item : qAsConst(chi)) {
206     if (top_level_item->kind() != RootItem::Kind::Bin &&
207         top_level_item->kind() != RootItem::Kind::Important &&
208         top_level_item->kind() != RootItem::Kind::Unread &&
209         top_level_item->kind() != RootItem::Kind::Labels) {
210       requestItemRemoval(top_level_item);
211     }
212   }
213 
214   if (labelsNode() != nullptr && clean_labels_too) {
215     auto lbl_chi = labelsNode()->childItems();
216 
217     for (RootItem* lbl : qAsConst(lbl_chi)) {
218       requestItemRemoval(lbl);
219     }
220   }
221 }
222 
appendCommonNodes()223 void ServiceRoot::appendCommonNodes() {
224   if (recycleBin() != nullptr && !childItems().contains(recycleBin())) {
225     appendChild(recycleBin());
226   }
227 
228   if (importantNode() != nullptr && !childItems().contains(importantNode())) {
229     appendChild(importantNode());
230   }
231 
232   if (unreadNode() != nullptr && !childItems().contains(unreadNode())) {
233     appendChild(unreadNode());
234   }
235 
236   if (labelsNode() != nullptr && !childItems().contains(labelsNode())) {
237     appendChild(labelsNode());
238   }
239 }
240 
cleanFeeds(const QList<Feed * > & items,bool clean_read_only)241 bool ServiceRoot::cleanFeeds(const QList<Feed*>& items, bool clean_read_only) {
242   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
243 
244   if (DatabaseQueries::cleanFeeds(database, textualFeedIds(items), clean_read_only, accountId())) {
245     getParentServiceRoot()->updateCounts(true);
246     getParentServiceRoot()->itemChanged(getParentServiceRoot()->getSubTree());
247     getParentServiceRoot()->requestReloadMessageList(true);
248     return true;
249   }
250   else {
251     return false;
252   }
253 }
254 
storeNewFeedTree(RootItem * root)255 void ServiceRoot::storeNewFeedTree(RootItem* root) {
256   try {
257     DatabaseQueries::storeAccountTree(qApp->database()->driver()->connection(metaObject()->className()), root, accountId());
258   }
259   catch (const ApplicationException& ex) {
260     qFatal("Cannot store account tree: '%s'.", qPrintable(ex.message()));
261   }
262 }
263 
removeLeftOverMessages()264 void ServiceRoot::removeLeftOverMessages() {
265   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
266 
267   DatabaseQueries::purgeLeftoverMessages(database, accountId());
268 }
269 
removeLeftOverMessageFilterAssignments()270 void ServiceRoot::removeLeftOverMessageFilterAssignments() {
271   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
272 
273   DatabaseQueries::purgeLeftoverMessageFilterAssignments(database, accountId());
274 }
275 
removeLeftOverMessageLabelAssignments()276 void ServiceRoot::removeLeftOverMessageLabelAssignments() {
277   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
278 
279   DatabaseQueries::purgeLeftoverLabelAssignments(database, accountId());
280 }
281 
undeletedMessages() const282 QList<Message> ServiceRoot::undeletedMessages() const {
283   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
284 
285   return DatabaseQueries::getUndeletedMessagesForAccount(database, accountId());
286 }
287 
supportsFeedAdding() const288 bool ServiceRoot::supportsFeedAdding() const {
289   return false;
290 }
291 
supportsCategoryAdding() const292 bool ServiceRoot::supportsCategoryAdding() const {
293   return false;
294 }
295 
supportedLabelOperations() const296 ServiceRoot::LabelOperation ServiceRoot::supportedLabelOperations() const {
297   return LabelOperation::Adding | LabelOperation::Editing | LabelOperation::Deleting;
298 }
299 
saveAccountDataToDatabase()300 void ServiceRoot::saveAccountDataToDatabase() {
301   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
302 
303   try {
304     DatabaseQueries::createOverwriteAccount(database, this);
305   }
306   catch (const ApplicationException& ex) {
307     qFatal("Account was not saved into database: '%s'.", qPrintable(ex.message()));
308   }
309 }
310 
customDatabaseData() const311 QVariantHash ServiceRoot::customDatabaseData() const {
312   return {};
313 }
314 
setCustomDatabaseData(const QVariantHash & data)315 void ServiceRoot::setCustomDatabaseData(const QVariantHash& data) {
316   Q_UNUSED(data)
317 }
318 
wantsBaggedIdsOfExistingMessages() const319 bool ServiceRoot::wantsBaggedIdsOfExistingMessages() const {
320   return false;
321 }
322 
aboutToBeginFeedFetching(const QList<Feed * > & feeds,const QHash<QString,QHash<BagOfMessages,QStringList>> & stated_messages,const QHash<QString,QStringList> & tagged_messages)323 void ServiceRoot::aboutToBeginFeedFetching(const QList<Feed*>& feeds,
324                                            const QHash<QString, QHash<BagOfMessages, QStringList>>& stated_messages,
325                                            const QHash<QString, QStringList>& tagged_messages) {
326   Q_UNUSED(feeds)
327   Q_UNUSED(stated_messages)
328   Q_UNUSED(tagged_messages)
329 }
330 
itemChanged(const QList<RootItem * > & items)331 void ServiceRoot::itemChanged(const QList<RootItem*>& items) {
332   emit dataChanged(items);
333 }
334 
requestReloadMessageList(bool mark_selected_messages_read)335 void ServiceRoot::requestReloadMessageList(bool mark_selected_messages_read) {
336   emit reloadMessageListRequested(mark_selected_messages_read);
337 }
338 
requestItemExpand(const QList<RootItem * > & items,bool expand)339 void ServiceRoot::requestItemExpand(const QList<RootItem*>& items, bool expand) {
340   emit itemExpandRequested(items, expand);
341 }
342 
requestItemExpandStateSave(RootItem * subtree_root)343 void ServiceRoot::requestItemExpandStateSave(RootItem* subtree_root) {
344   emit itemExpandStateSaveRequested(subtree_root);
345 }
346 
requestItemReassignment(RootItem * item,RootItem * new_parent)347 void ServiceRoot::requestItemReassignment(RootItem* item, RootItem* new_parent) {
348   emit itemReassignmentRequested(item, new_parent);
349 }
350 
requestItemRemoval(RootItem * item)351 void ServiceRoot::requestItemRemoval(RootItem* item) {
352   emit itemRemovalRequested(item);
353 }
354 
addNewFeed(RootItem * selected_item,const QString & url)355 void ServiceRoot::addNewFeed(RootItem* selected_item, const QString& url) {
356   Q_UNUSED(selected_item)
357   Q_UNUSED(url)
358 }
359 
addNewCategory(RootItem * selected_item)360 void ServiceRoot::addNewCategory(RootItem* selected_item) {
361   Q_UNUSED(selected_item)
362 }
363 
storeCustomFeedsData()364 QMap<QString, QVariantMap> ServiceRoot::storeCustomFeedsData() {
365   QMap<QString, QVariantMap> custom_data;
366   auto str = getSubTreeFeeds();
367 
368   for (const Feed* feed : qAsConst(str)) {
369     QVariantMap feed_custom_data;
370 
371     // TODO: This could potentially call Feed::customDatabaseData() and append it
372     // to this map and also subsequently restore.
373     feed_custom_data.insert(QSL("auto_update_interval"), feed->autoUpdateInitialInterval());
374     feed_custom_data.insert(QSL("auto_update_type"), int(feed->autoUpdateType()));
375     feed_custom_data.insert(QSL("msg_filters"), QVariant::fromValue(feed->messageFilters()));
376     custom_data.insert(feed->customId(), feed_custom_data);
377   }
378 
379   return custom_data;
380 }
381 
restoreCustomFeedsData(const QMap<QString,QVariantMap> & data,const QHash<QString,Feed * > & feeds)382 void ServiceRoot::restoreCustomFeedsData(const QMap<QString, QVariantMap>& data, const QHash<QString, Feed*>& feeds) {
383   QMapIterator<QString, QVariantMap> i(data);
384 
385   while (i.hasNext()) {
386     i.next();
387     const QString custom_id = i.key();
388 
389     if (feeds.contains(custom_id)) {
390       Feed* feed = feeds.value(custom_id);
391       QVariantMap feed_custom_data = i.value();
392 
393       feed->setAutoUpdateInitialInterval(feed_custom_data.value(QSL("auto_update_interval")).toInt());
394       feed->setAutoUpdateType(static_cast<Feed::AutoUpdateType>(feed_custom_data.value(QSL("auto_update_type")).toInt()));
395       feed->setMessageFilters(feed_custom_data.value(QSL("msg_filters")).value<QList<QPointer<MessageFilter>>>());
396     }
397   }
398 }
399 
networkProxy() const400 QNetworkProxy ServiceRoot::networkProxy() const {
401   return m_networkProxy;
402 }
403 
setNetworkProxy(const QNetworkProxy & network_proxy)404 void ServiceRoot::setNetworkProxy(const QNetworkProxy& network_proxy) {
405   m_networkProxy = network_proxy;
406 
407   emit proxyChanged(network_proxy);
408 }
409 
importantNode() const410 ImportantNode* ServiceRoot::importantNode() const {
411   return m_importantNode;
412 }
413 
labelsNode() const414 LabelsNode* ServiceRoot::labelsNode() const {
415   return m_labelsNode;
416 }
417 
unreadNode() const418 UnreadNode* ServiceRoot::unreadNode() const {
419   return m_unreadNode;
420 }
421 
syncIn()422 void ServiceRoot::syncIn() {
423   QIcon original_icon = icon();
424 
425   setIcon(qApp->icons()->fromTheme(QSL("view-refresh")));
426   itemChanged(QList<RootItem*>() << this);
427   RootItem* new_tree = obtainNewTreeForSyncIn();
428 
429   if (new_tree != nullptr) {
430     auto feed_custom_data = storeCustomFeedsData();
431 
432     // Remove from feeds model, then from SQL but leave messages intact.
433     bool uses_remote_labels = (supportedLabelOperations() & LabelOperation::Synchronised) == LabelOperation::Synchronised;
434 
435     // Remove stuff.
436     cleanAllItemsFromModel(uses_remote_labels);
437     removeOldAccountFromDatabase(false, uses_remote_labels);
438 
439     // Restore some local settings to feeds etc.
440     restoreCustomFeedsData(feed_custom_data, new_tree->getHashedSubTreeFeeds());
441 
442     // Model is clean, now store new tree into DB and
443     // set primary IDs of the items.
444     storeNewFeedTree(new_tree);
445 
446     // We have new feed, some feeds were maybe removed,
447     // so remove left over messages and filter assignments.
448     removeLeftOverMessages();
449     removeLeftOverMessageFilterAssignments();
450     removeLeftOverMessageLabelAssignments();
451 
452     auto chi = new_tree->childItems();
453 
454     for (RootItem* top_level_item : qAsConst(chi)) {
455       if (top_level_item->kind() != Kind::Labels) {
456         top_level_item->setParent(nullptr);
457         requestItemReassignment(top_level_item, this);
458       }
459       else {
460         // It seems that some labels got synced-in.
461         if (labelsNode() != nullptr) {
462           auto lbl_chi = top_level_item->childItems();
463 
464           for (RootItem* new_lbl : qAsConst(lbl_chi)) {
465             new_lbl->setParent(nullptr);
466             requestItemReassignment(new_lbl, labelsNode());
467           }
468         }
469       }
470     }
471 
472     new_tree->clearChildren();
473     new_tree->deleteLater();
474 
475     updateCounts(true);
476     requestReloadMessageList(true);
477   }
478 
479   setIcon(original_icon);
480   itemChanged(getSubTree());
481   requestItemExpand(getSubTree(), true);
482 }
483 
performInitialAssembly(const Assignment & categories,const Assignment & feeds,const QList<Label * > & labels)484 void ServiceRoot::performInitialAssembly(const Assignment& categories,
485                                          const Assignment& feeds,
486                                          const QList<Label*>& labels) {
487   assembleCategories(categories);
488   assembleFeeds(feeds);
489   labelsNode()->loadLabels(labels);
490   updateCounts(true);
491 }
492 
obtainNewTreeForSyncIn() const493 RootItem* ServiceRoot::obtainNewTreeForSyncIn() const {
494   return nullptr;
495 }
496 
customIDSOfMessagesForItem(RootItem * item)497 QStringList ServiceRoot::customIDSOfMessagesForItem(RootItem* item) {
498   if (item->getParentServiceRoot() != this) {
499     // Not item from this account.
500     return QStringList();
501   }
502   else {
503     QStringList list;
504 
505     switch (item->kind()) {
506       case RootItem::Kind::Labels:
507       case RootItem::Kind::Category: {
508         auto chi = item->childItems();
509 
510         for (RootItem* child : qAsConst(chi)) {
511           list.append(customIDSOfMessagesForItem(child));
512         }
513 
514         return list;
515       }
516 
517       case RootItem::Kind::Label: {
518         QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
519 
520         list = DatabaseQueries::customIdsOfMessagesFromLabel(database, item->toLabel());
521         break;
522       }
523 
524       case RootItem::Kind::ServiceRoot: {
525         QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
526 
527         list = DatabaseQueries::customIdsOfMessagesFromAccount(database, accountId());
528         break;
529       }
530 
531       case RootItem::Kind::Bin: {
532         QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
533 
534         list = DatabaseQueries::customIdsOfMessagesFromBin(database, accountId());
535         break;
536       }
537 
538       case RootItem::Kind::Feed: {
539         QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
540 
541         list = DatabaseQueries::customIdsOfMessagesFromFeed(database, item->customId(), accountId());
542         break;
543       }
544 
545       case RootItem::Kind::Important: {
546         QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
547 
548         list = DatabaseQueries::customIdsOfImportantMessages(database, accountId());
549         break;
550       }
551 
552       case RootItem::Kind::Unread: {
553         QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
554 
555         list = DatabaseQueries::customIdsOfUnreadMessages(database, accountId());
556         break;
557       }
558 
559       default:
560         break;
561     }
562 
563     qDebug() << "Custom IDs of messages for some operation are:" << list;
564     return list;
565   }
566 }
567 
markFeedsReadUnread(const QList<Feed * > & items,RootItem::ReadStatus read)568 bool ServiceRoot::markFeedsReadUnread(const QList<Feed*>& items, RootItem::ReadStatus read) {
569   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
570 
571   if (DatabaseQueries::markFeedsReadUnread(database, textualFeedIds(items), accountId(), read)) {
572     getParentServiceRoot()->updateCounts(false);
573     getParentServiceRoot()->itemChanged(getParentServiceRoot()->getSubTree());
574     getParentServiceRoot()->requestReloadMessageList(read == RootItem::ReadStatus::Read);
575     return true;
576   }
577   else {
578     return false;
579   }
580 }
581 
textualFeedUrls(const QList<Feed * > & feeds) const582 QStringList ServiceRoot::textualFeedUrls(const QList<Feed*>& feeds) const {
583   QStringList stringy_urls;
584 
585   stringy_urls.reserve(feeds.size());
586 
587   for (const Feed* feed : feeds) {
588     stringy_urls.append(!feed->source().isEmpty() ? feed->source() : QSL("no-url"));
589   }
590 
591   return stringy_urls;
592 }
593 
textualFeedIds(const QList<Feed * > & feeds) const594 QStringList ServiceRoot::textualFeedIds(const QList<Feed*>& feeds) const {
595   QStringList stringy_ids; stringy_ids.reserve(feeds.size());
596 
597   for (const Feed* feed : feeds) {
598     stringy_ids.append(QSL("'%1'").arg(feed->customId()));
599   }
600 
601   return stringy_ids;
602 }
603 
customIDsOfMessages(const QList<ImportanceChange> & changes)604 QStringList ServiceRoot::customIDsOfMessages(const QList<ImportanceChange>& changes) {
605   QStringList list; list.reserve(changes.size());
606 
607   for (const auto& change : changes) {
608     list.append(change.first.m_customId);
609   }
610 
611   return list;
612 }
613 
customIDsOfMessages(const QList<Message> & messages)614 QStringList ServiceRoot::customIDsOfMessages(const QList<Message>& messages) {
615   QStringList list; list.reserve(messages.size());
616 
617   for (const Message& message : messages) {
618     list.append(message.m_customId);
619   }
620 
621   return list;
622 }
623 
accountId() const624 int ServiceRoot::accountId() const {
625   return m_accountId;
626 }
627 
setAccountId(int account_id)628 void ServiceRoot::setAccountId(int account_id) {
629   m_accountId = account_id;
630 
631   auto* cache = dynamic_cast<CacheForServiceRoot*>(this);
632 
633   if (cache != nullptr) {
634     cache->setUniqueId(account_id);
635   }
636 }
637 
loadMessagesForItem(RootItem * item,MessagesModel * model)638 bool ServiceRoot::loadMessagesForItem(RootItem* item, MessagesModel* model) {
639   if (item->kind() == RootItem::Kind::Bin) {
640     model->setFilter(QSL("Messages.is_deleted = 1 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1")
641                      .arg(QString::number(accountId())));
642   }
643   else if (item->kind() == RootItem::Kind::Important) {
644     model->setFilter(QSL("Messages.is_important = 1 AND Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1")
645                      .arg(QString::number(accountId())));
646   }
647   else if (item->kind() == RootItem::Kind::Unread) {
648     model->setFilter(QSL("Messages.is_read = 0 AND Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1")
649                      .arg(QString::number(accountId())));
650   }
651   else if (item->kind() == RootItem::Kind::Label) {
652     // Show messages with particular label.
653     model->setFilter(QSL("Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1 AND "
654                          "(SELECT COUNT(*) FROM LabelsInMessages WHERE account_id = %1 AND message = Messages.custom_id AND label = '%2') > 0")
655                      .arg(QString::number(accountId()), item->customId()));
656   }
657   else if (item->kind() == RootItem::Kind::Labels) {
658     // Show messages with any label.
659     model->setFilter(QSL("Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1 AND "
660                          "(SELECT COUNT(*) FROM LabelsInMessages WHERE account_id = %1 AND message = Messages.custom_id) > 0")
661                      .arg(QString::number(accountId())));
662   }
663   else if (item->kind() == RootItem::Kind::ServiceRoot) {
664     model->setFilter(
665       QSL("Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %1").arg(
666         QString::number(accountId())));
667 
668     qDebugNN << "Displaying messages from account:" << QUOTE_W_SPACE_DOT(accountId());
669   }
670   else {
671     QList<Feed*> children = item->getSubTreeFeeds();
672     QString filter_clause = textualFeedIds(children).join(QSL(", "));
673 
674     if (filter_clause.isEmpty()) {
675       filter_clause = QSL("null");
676     }
677 
678     model->setFilter(
679       QSL("Feeds.custom_id IN (%1) AND Messages.is_deleted = 0 AND Messages.is_pdeleted = 0 AND Messages.account_id = %2").arg(
680         filter_clause,
681         QString::
682         number(accountId())));
683 
684     QString urls = textualFeedUrls(children).join(QSL(", "));
685 
686     qDebugNN << "Displaying messages from feeds IDs:" << QUOTE_W_SPACE(filter_clause)
687              << "and URLs:" << QUOTE_W_SPACE_DOT(urls);
688   }
689 
690   return true;
691 }
692 
onBeforeSetMessagesRead(RootItem * selected_item,const QList<Message> & messages,RootItem::ReadStatus read)693 bool ServiceRoot::onBeforeSetMessagesRead(RootItem* selected_item, const QList<Message>& messages, RootItem::ReadStatus read) {
694   Q_UNUSED(selected_item)
695 
696   auto cache = dynamic_cast<CacheForServiceRoot*>(this);
697 
698   if (cache != nullptr) {
699     cache->addMessageStatesToCache(customIDsOfMessages(messages), read);
700   }
701 
702   return true;
703 }
704 
onAfterSetMessagesRead(RootItem * selected_item,const QList<Message> & messages,RootItem::ReadStatus read)705 bool ServiceRoot::onAfterSetMessagesRead(RootItem* selected_item, const QList<Message>& messages, RootItem::ReadStatus read) {
706   Q_UNUSED(selected_item)
707   Q_UNUSED(messages)
708   Q_UNUSED(read)
709 
710   updateCounts(true);
711   itemChanged(getSubTree());
712   return true;
713 }
714 
onBeforeSwitchMessageImportance(RootItem * selected_item,const QList<ImportanceChange> & changes)715 bool ServiceRoot::onBeforeSwitchMessageImportance(RootItem* selected_item, const QList<ImportanceChange>& changes) {
716   Q_UNUSED(selected_item)
717 
718   auto cache = dynamic_cast<CacheForServiceRoot*>(this);
719 
720   if (cache != nullptr) {
721     // Now, we need to separate the changes because of Nextcloud API limitations.
722     QList<Message> mark_starred_msgs;
723     QList<Message> mark_unstarred_msgs;
724 
725     for (const ImportanceChange& pair : changes) {
726       if (pair.second == RootItem::Importance::Important) {
727         mark_starred_msgs.append(pair.first);
728       }
729       else {
730         mark_unstarred_msgs.append(pair.first);
731       }
732     }
733 
734     if (!mark_starred_msgs.isEmpty()) {
735       cache->addMessageStatesToCache(mark_starred_msgs, RootItem::Importance::Important);
736     }
737 
738     if (!mark_unstarred_msgs.isEmpty()) {
739       cache->addMessageStatesToCache(mark_unstarred_msgs, RootItem::Importance::NotImportant);
740     }
741   }
742 
743   return true;
744 }
745 
onAfterSwitchMessageImportance(RootItem * selected_item,const QList<ImportanceChange> & changes)746 bool ServiceRoot::onAfterSwitchMessageImportance(RootItem* selected_item, const QList<ImportanceChange>& changes) {
747   Q_UNUSED(selected_item)
748   Q_UNUSED(changes)
749 
750   updateCounts(true);
751   itemChanged(getSubTree());
752   return true;
753 }
754 
onBeforeMessagesDelete(RootItem * selected_item,const QList<Message> & messages)755 bool ServiceRoot::onBeforeMessagesDelete(RootItem* selected_item, const QList<Message>& messages) {
756   Q_UNUSED(selected_item)
757   Q_UNUSED(messages)
758   return true;
759 }
760 
onAfterMessagesDelete(RootItem * selected_item,const QList<Message> & messages)761 bool ServiceRoot::onAfterMessagesDelete(RootItem* selected_item, const QList<Message>& messages) {
762   Q_UNUSED(selected_item)
763   Q_UNUSED(messages)
764 
765   updateCounts(true);
766   itemChanged(getSubTree());
767   return true;
768 }
769 
onBeforeLabelMessageAssignmentChanged(const QList<Label * > & labels,const QList<Message> & messages,bool assign)770 bool ServiceRoot::onBeforeLabelMessageAssignmentChanged(const QList<Label*>& labels,
771                                                         const QList<Message>& messages,
772                                                         bool assign) {
773   auto cache = dynamic_cast<CacheForServiceRoot*>(this);
774 
775   if (cache != nullptr) {
776     boolinq::from(labels).for_each([cache, messages, assign](Label* lbl) {
777       cache->addLabelsAssignmentsToCache(messages, lbl, assign);
778     });
779   }
780 
781   return true;
782 }
783 
onAfterLabelMessageAssignmentChanged(const QList<Label * > & labels,const QList<Message> & messages,bool assign)784 bool ServiceRoot::onAfterLabelMessageAssignmentChanged(const QList<Label*>& labels,
785                                                        const QList<Message>& messages,
786                                                        bool assign) {
787   Q_UNUSED(messages)
788   Q_UNUSED(assign)
789 
790   boolinq::from(labels).for_each([](Label* lbl) {
791     lbl->updateCounts(true);
792   });
793 
794   auto list = boolinq::from(labels).select([](Label* lbl) {
795     return static_cast<RootItem*>(lbl);
796   }).toStdList();
797 
798   getParentServiceRoot()->itemChanged(FROM_STD_LIST(QList<RootItem*>, list));
799   return true;
800 }
801 
onBeforeMessagesRestoredFromBin(RootItem * selected_item,const QList<Message> & messages)802 bool ServiceRoot::onBeforeMessagesRestoredFromBin(RootItem* selected_item, const QList<Message>& messages) {
803   Q_UNUSED(selected_item)
804   Q_UNUSED(messages)
805 
806   return true;
807 }
808 
onAfterMessagesRestoredFromBin(RootItem * selected_item,const QList<Message> & messages)809 bool ServiceRoot::onAfterMessagesRestoredFromBin(RootItem* selected_item, const QList<Message>& messages) {
810   Q_UNUSED(selected_item)
811   Q_UNUSED(messages)
812 
813   updateCounts(true);
814   itemChanged(getSubTree());
815   return true;
816 }
817 
toCache() const818 CacheForServiceRoot* ServiceRoot::toCache() const {
819   return dynamic_cast<CacheForServiceRoot*>(const_cast<ServiceRoot*>(this));
820 }
821 
assembleFeeds(const Assignment & feeds)822 void ServiceRoot::assembleFeeds(const Assignment& feeds) {
823   QHash<int, Category*> categories = getHashedSubTreeCategories();
824 
825   for (const AssignmentItem& feed : feeds) {
826     if (feed.first == NO_PARENT_CATEGORY) {
827       // This is top-level feed, add it to the root item.
828       appendChild(feed.second);
829     }
830     else if (categories.contains(feed.first)) {
831       // This feed belongs to this category.
832       categories.value(feed.first)->appendChild(feed.second);
833     }
834     else {
835       qWarningNN << LOGSEC_CORE << "Feed" << QUOTE_W_SPACE(feed.second->title()) << "is loose, skipping it.";
836     }
837   }
838 }
839 
assembleCategories(const Assignment & categories)840 void ServiceRoot::assembleCategories(const Assignment& categories) {
841   Assignment editable_categories = categories;
842   QHash<int, RootItem*> assignments;
843 
844   assignments.insert(NO_PARENT_CATEGORY, this);
845 
846   // Add top-level categories.
847   while (!editable_categories.isEmpty()) {
848     for (int i = 0; i < editable_categories.size(); i++) {
849       if (assignments.contains(editable_categories.at(i).first)) {
850         // Parent category of this category is already added.
851         assignments.value(editable_categories.at(i).first)->appendChild(editable_categories.at(i).second);
852 
853         // Now, added category can be parent for another categories, add it.
854         assignments.insert(editable_categories.at(i).second->id(), editable_categories.at(i).second);
855 
856         // Remove the category from the list, because it was
857         // added to the final collection.
858         editable_categories.removeAt(i);
859         i--;
860       }
861     }
862   }
863 }
864 
operator |(ServiceRoot::LabelOperation lhs,ServiceRoot::LabelOperation rhs)865 ServiceRoot::LabelOperation operator|(ServiceRoot::LabelOperation lhs, ServiceRoot::LabelOperation rhs) {
866   return static_cast<ServiceRoot::LabelOperation>(static_cast<char>(lhs) | static_cast<char>(rhs));
867 }
868 
operator &(ServiceRoot::LabelOperation lhs,ServiceRoot::LabelOperation rhs)869 ServiceRoot::LabelOperation operator&(ServiceRoot::LabelOperation lhs, ServiceRoot::LabelOperation rhs) {
870   return static_cast<ServiceRoot::LabelOperation>(static_cast<char>(lhs) & static_cast<char>(rhs));
871 }
872 
updateMessages(QList<Message> & messages,Feed * feed,bool force_update)873 QPair<int, int> ServiceRoot::updateMessages(QList<Message>& messages, Feed* feed, bool force_update) {
874   QPair<int, int> updated_messages = { 0, 0 };
875 
876   if (messages.isEmpty()) {
877     qDebugNN << "No messages to be updated/added in DB for feed"
878              << QUOTE_W_SPACE_DOT(feed->customId());
879     return updated_messages;
880   }
881 
882   QList<RootItem*> items_to_update;
883   bool is_main_thread = QThread::currentThread() == qApp->thread();
884 
885   qDebugNN << LOGSEC_CORE
886            << "Updating messages in DB. Main thread:"
887            << QUOTE_W_SPACE_DOT(is_main_thread);
888 
889   bool ok = false;
890   QSqlDatabase database = is_main_thread ?
891                           qApp->database()->driver()->connection(metaObject()->className()) :
892                           qApp->database()->driver()->connection(QSL("feed_upd"));
893 
894   updated_messages = DatabaseQueries::updateMessages(database, messages, feed, force_update, &ok);
895 
896   if (updated_messages.first > 0 || updated_messages.second > 0) {
897     // Something was added or updated in the DB, update numbers.
898     feed->updateCounts(true);
899 
900     if (recycleBin() != nullptr) {
901       recycleBin()->updateCounts(true);
902       items_to_update.append(recycleBin());
903     }
904 
905     if (importantNode() != nullptr) {
906       importantNode()->updateCounts(true);
907       items_to_update.append(importantNode());
908     }
909 
910     if (unreadNode() != nullptr) {
911       unreadNode()->updateCounts(true);
912       items_to_update.append(unreadNode());
913     }
914 
915     if (labelsNode() != nullptr) {
916       labelsNode()->updateCounts(true);
917       items_to_update.append(labelsNode());
918     }
919   }
920 
921   // Some messages were really added to DB, reload feed in model.
922   items_to_update.append(feed);
923   getParentServiceRoot()->itemChanged(items_to_update);
924 
925   return updated_messages;
926 }
927