1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include "services/tt-rss/ttrssserviceroot.h"
4 
5 #include "database/databasequeries.h"
6 #include "exceptions/feedfetchexception.h"
7 #include "miscellaneous/application.h"
8 #include "miscellaneous/iconfactory.h"
9 #include "miscellaneous/mutex.h"
10 #include "miscellaneous/settings.h"
11 #include "miscellaneous/textfactory.h"
12 #include "network-web/networkfactory.h"
13 #include "services/abstract/importantnode.h"
14 #include "services/abstract/labelsnode.h"
15 #include "services/abstract/recyclebin.h"
16 #include "services/tt-rss/definitions.h"
17 #include "services/tt-rss/gui/formeditttrssaccount.h"
18 #include "services/tt-rss/gui/formttrssfeeddetails.h"
19 #include "services/tt-rss/ttrssfeed.h"
20 #include "services/tt-rss/ttrssnetworkfactory.h"
21 #include "services/tt-rss/ttrssserviceentrypoint.h"
22 
23 #include <QClipboard>
24 #include <QPair>
25 #include <QSqlTableModel>
26 
TtRssServiceRoot(RootItem * parent)27 TtRssServiceRoot::TtRssServiceRoot(RootItem* parent)
28   : ServiceRoot(parent), m_network(new TtRssNetworkFactory()) {
29   setIcon(TtRssServiceEntryPoint().icon());
30 }
31 
~TtRssServiceRoot()32 TtRssServiceRoot::~TtRssServiceRoot() {
33   delete m_network;
34 }
35 
supportedLabelOperations() const36 ServiceRoot::LabelOperation TtRssServiceRoot::supportedLabelOperations() const {
37   return ServiceRoot::LabelOperation::Synchronised;
38 }
39 
start(bool freshly_activated)40 void TtRssServiceRoot::start(bool freshly_activated) {
41   if (!freshly_activated) {
42     DatabaseQueries::loadFromDatabase<Category, TtRssFeed>(this);
43     loadCacheFromFile();
44   }
45 
46   updateTitle();
47 
48   if (getSubTreeFeeds().isEmpty()) {
49     syncIn();
50   }
51 }
52 
stop()53 void TtRssServiceRoot::stop() {
54   m_network->logout(networkProxy());
55   qDebugNN << LOGSEC_TTRSS
56            << "Stopping Tiny Tiny RSS account, logging out with result"
57            << QUOTE_W_SPACE_DOT(m_network->lastError());
58 }
59 
code() const60 QString TtRssServiceRoot::code() const {
61   return TtRssServiceEntryPoint().code();
62 }
63 
isSyncable() const64 bool TtRssServiceRoot::isSyncable() const {
65   return true;
66 }
67 
editViaGui()68 bool TtRssServiceRoot::editViaGui() {
69   QScopedPointer<FormEditTtRssAccount> form_pointer(new FormEditTtRssAccount(qApp->mainFormWidget()));
70 
71   form_pointer->addEditAccount(this);
72   return true;
73 }
74 
supportsFeedAdding() const75 bool TtRssServiceRoot::supportsFeedAdding() const {
76   return true;
77 }
78 
supportsCategoryAdding() const79 bool TtRssServiceRoot::supportsCategoryAdding() const {
80   return false;
81 }
82 
addNewFeed(RootItem * selected_item,const QString & url)83 void TtRssServiceRoot::addNewFeed(RootItem* selected_item, const QString& url) {
84   if (!qApp->feedUpdateLock()->tryLock()) {
85     // Lock was not obtained because
86     // it is used probably by feed updater or application
87     // is quitting.
88     qApp->showGuiMessage(Notification::Event::GeneralEvent,
89                          tr("Cannot add item"),
90                          tr("Cannot add feed because another critical operation is ongoing."),
91                          QSystemTrayIcon::MessageIcon::Warning,
92                          true);
93 
94     return;
95   }
96 
97   QScopedPointer<FormTtRssFeedDetails> form_pointer(new FormTtRssFeedDetails(this, selected_item, url, qApp->mainFormWidget()));
98 
99   form_pointer->addEditFeed<TtRssFeed>();
100   qApp->feedUpdateLock()->unlock();
101 }
102 
canBeEdited() const103 bool TtRssServiceRoot::canBeEdited() const {
104   return true;
105 }
106 
saveAllCachedData(bool ignore_errors)107 void TtRssServiceRoot::saveAllCachedData(bool ignore_errors) {
108   auto msg_cache = takeMessageCache();
109   QMapIterator<RootItem::ReadStatus, QStringList> i(msg_cache.m_cachedStatesRead);
110 
111   // Save the actual data read/unread.
112   while (i.hasNext()) {
113     i.next();
114     auto key = i.key();
115     QStringList ids = i.value();
116 
117     if (!ids.isEmpty()) {
118       auto res = network()->updateArticles(ids,
119                                            UpdateArticle::OperatingField::Unread,
120                                            key == RootItem::ReadStatus::Unread
121                                            ? UpdateArticle::Mode::SetToTrue
122                                            : UpdateArticle::Mode::SetToFalse,
123                                            networkProxy());
124 
125       if (!ignore_errors && (network()->lastError() != QNetworkReply::NetworkError::NoError || res.hasError())) {
126         addMessageStatesToCache(ids, key);
127       }
128     }
129   }
130 
131   QMapIterator<RootItem::Importance, QList<Message>> j(msg_cache.m_cachedStatesImportant);
132 
133   // Save the actual data important/not important.
134   while (j.hasNext()) {
135     j.next();
136     auto key = j.key();
137     QList<Message> messages = j.value();
138 
139     if (!messages.isEmpty()) {
140       QStringList ids = customIDsOfMessages(messages);
141       auto res = network()->updateArticles(ids,
142                                            UpdateArticle::OperatingField::Starred,
143                                            key == RootItem::Importance::Important
144                                            ? UpdateArticle::Mode::SetToTrue
145                                            : UpdateArticle::Mode::SetToFalse,
146                                            networkProxy());
147 
148       if (!ignore_errors && (network()->lastError() != QNetworkReply::NetworkError::NoError || res.hasError())) {
149         addMessageStatesToCache(messages, key);
150       }
151     }
152   }
153 
154   QMapIterator<QString, QStringList> k(msg_cache.m_cachedLabelAssignments);
155 
156   // Assign label for these messages.
157   while (k.hasNext()) {
158     k.next();
159     auto label_custom_id = k.key();
160     QStringList messages = k.value();
161 
162     if (!messages.isEmpty()) {
163       auto res = network()->setArticleLabel(messages, label_custom_id, true, networkProxy());
164 
165       if (!ignore_errors && (network()->lastError() != QNetworkReply::NetworkError::NoError || res.hasError())) {
166         addLabelsAssignmentsToCache(messages, label_custom_id, true);
167       }
168     }
169   }
170 
171   QMapIterator<QString, QStringList> l(msg_cache.m_cachedLabelDeassignments);
172 
173   // Remove label from these messages.
174   while (l.hasNext()) {
175     l.next();
176     auto label_custom_id = l.key();
177     QStringList messages = l.value();
178 
179     if (!messages.isEmpty()) {
180       auto res = network()->setArticleLabel(messages, label_custom_id, false, networkProxy());
181 
182       if (!ignore_errors && (network()->lastError() != QNetworkReply::NetworkError::NoError || res.hasError())) {
183         addLabelsAssignmentsToCache(messages, label_custom_id, false);
184       }
185     }
186   }
187 }
188 
customDatabaseData() const189 QVariantHash TtRssServiceRoot::customDatabaseData() const {
190   QVariantHash data;
191 
192   data[QSL("username")] = m_network->username();
193   data[QSL("password")] = TextFactory::encrypt(m_network->password());
194   data[QSL("auth_protected")] = m_network->authIsUsed();
195   data[QSL("auth_username")] = m_network->authUsername();
196   data[QSL("auth_password")] = TextFactory::encrypt(m_network->authPassword());
197   data[QSL("url")] = m_network->url();
198   data[QSL("force_update")] = m_network->forceServerSideUpdate();
199   data[QSL("batch_size")] = m_network->batchSize();
200   data[QSL("download_only_unread")] = m_network->downloadOnlyUnreadMessages();
201 
202   return data;
203 }
204 
setCustomDatabaseData(const QVariantHash & data)205 void TtRssServiceRoot::setCustomDatabaseData(const QVariantHash& data) {
206   m_network->setUsername(data[QSL("username")].toString());
207   m_network->setPassword(TextFactory::decrypt(data[QSL("password")].toString()));
208   m_network->setAuthIsUsed(data[QSL("auth_protected")].toBool());
209   m_network->setAuthUsername(data[QSL("auth_username")].toString());
210   m_network->setAuthPassword(TextFactory::decrypt(data[QSL("auth_password")].toString()));
211   m_network->setUrl(data[QSL("url")].toString());
212   m_network->setForceServerSideUpdate(data[QSL("force_update")].toBool());
213   m_network->setBatchSize(data[QSL("batch_size")].toInt());
214   m_network->setDownloadOnlyUnreadMessages(data[QSL("download_only_unread")].toBool());
215 }
216 
obtainNewMessages(Feed * feed,const QHash<ServiceRoot::BagOfMessages,QStringList> & stated_messages,const QHash<QString,QStringList> & tagged_messages)217 QList<Message> TtRssServiceRoot::obtainNewMessages(Feed* feed,
218                                                    const QHash<ServiceRoot::BagOfMessages, QStringList>& stated_messages,
219                                                    const QHash<QString, QStringList>& tagged_messages) {
220   Q_UNUSED(stated_messages)
221   Q_UNUSED(tagged_messages)
222 
223   QList<Message> messages;
224   int newly_added_messages = 0;
225   int limit = network()->batchSize() <= 0 ? TTRSS_MAX_MESSAGES : network()->batchSize();
226   int skip = 0;
227 
228   do {
229     TtRssGetHeadlinesResponse headlines = network()->getHeadlines(feed->customNumericId(), limit, skip,
230                                                                   true, true, false,
231                                                                   network()->downloadOnlyUnreadMessages(),
232                                                                   networkProxy());
233 
234     if (network()->lastError() != QNetworkReply::NetworkError::NoError) {
235       throw FeedFetchException(Feed::Status::NetworkError, headlines.error());
236     }
237     else {
238       QList<Message> new_messages = headlines.messages(this);
239 
240       messages << new_messages;
241       newly_added_messages = new_messages.size();
242       skip += newly_added_messages;
243     }
244   }
245   while (newly_added_messages > 0 && (network()->batchSize() <= 0 || messages.size() < network()->batchSize()));
246 
247   return messages;
248 }
249 
additionalTooltip() const250 QString TtRssServiceRoot::additionalTooltip() const {
251   return tr("Username: %1\nServer: %2\n"
252             "Last error: %3\nLast login on: %4").arg(m_network->username(),
253                                                      m_network->url(),
254                                                      NetworkFactory::networkErrorText(m_network->lastError()),
255                                                      m_network->lastLoginTime().isValid()
256                                                      ? QLocale().toString(m_network->lastLoginTime(), QLocale::FormatType::ShortFormat)
257                                                      : QSL("-"));
258 }
259 
network() const260 TtRssNetworkFactory* TtRssServiceRoot::network() const {
261   return m_network;
262 }
263 
updateTitle()264 void TtRssServiceRoot::updateTitle() {
265   QString host = QUrl(m_network->url()).host();
266 
267   if (host.isEmpty()) {
268     host = m_network->url();
269   }
270 
271   setTitle(TextFactory::extractUsernameFromEmail(m_network->username()) + QSL(" (Tiny Tiny RSS)"));
272 }
273 
obtainNewTreeForSyncIn() const274 RootItem* TtRssServiceRoot::obtainNewTreeForSyncIn() const {
275   TtRssGetFeedsCategoriesResponse feed_cats = m_network->getFeedsCategories(networkProxy());
276   TtRssGetLabelsResponse labels = m_network->getLabels(networkProxy());
277 
278   if (m_network->lastError() == QNetworkReply::NoError) {
279     auto* tree = feed_cats.feedsCategories(true, networkProxy(), m_network->url());
280     auto* lblroot = new LabelsNode(tree);
281 
282     lblroot->setChildItems(labels.labels());
283     tree->appendChild(lblroot);
284 
285     return tree;
286   }
287   else {
288     return nullptr;
289   }
290 }
291