1 // For license of this file, see <project-root-folder>/LICENSE.md.
2 
3 #include <QDateTime>
4 #include <QJSEngine>
5 #include <QProcess>
6 
7 #include "gui/dialogs/formmessagefiltersmanager.h"
8 
9 #include "3rd-party/boolinq/boolinq.h"
10 #include "core/messagefilter.h"
11 #include "core/messagesforfiltersmodel.h"
12 #include "database/databasequeries.h"
13 #include "exceptions/filteringexception.h"
14 #include "gui/guiutilities.h"
15 #include "gui/messagebox.h"
16 #include "miscellaneous/application.h"
17 #include "miscellaneous/feedreader.h"
18 #include "miscellaneous/iconfactory.h"
19 #include "network-web/webfactory.h"
20 #include "services/abstract/accountcheckmodel.h"
21 #include "services/abstract/feed.h"
22 #include "services/abstract/labelsnode.h"
23 
FormMessageFiltersManager(FeedReader * reader,const QList<ServiceRoot * > & accounts,QWidget * parent)24 FormMessageFiltersManager::FormMessageFiltersManager(FeedReader* reader, const QList<ServiceRoot*>& accounts, QWidget* parent)
25   : QDialog(parent), m_feedsModel(new AccountCheckSortedModel(this)), m_rootItem(new RootItem()),
26   m_accounts(accounts), m_reader(reader), m_loadingFilter(false), m_msgModel(new MessagesForFiltersModel(this)) {
27   m_ui.setupUi(this);
28 
29   std::sort(m_accounts.begin(), m_accounts.end(), [](const ServiceRoot* lhs, const ServiceRoot* rhs) {
30     return lhs->title().compare(rhs->title(), Qt::CaseSensitivity::CaseInsensitive) < 0;
31   });
32 
33   m_ui.m_treeExistingMessages->setModel(m_msgModel);
34 
35   GuiUtilities::applyDialogProperties(*this, qApp->icons()->fromTheme(QSL("view-list-details")));
36 
37   m_ui.m_treeFeeds->setIndentation(FEEDS_VIEW_INDENTATION);
38   m_ui.m_treeFeeds->setModel(m_feedsModel);
39   m_ui.m_btnCheckAll->setIcon(qApp->icons()->fromTheme(QSL("dialog-yes")));
40   m_ui.m_btnUncheckAll->setIcon(qApp->icons()->fromTheme(QSL("dialog-no")));
41   m_ui.m_btnAddNew->setIcon(qApp->icons()->fromTheme(QSL("list-add")));
42   m_ui.m_btnRemoveSelected->setIcon(qApp->icons()->fromTheme(QSL("list-remove")));
43   m_ui.m_btnBeautify->setIcon(qApp->icons()->fromTheme(QSL("format-justify-fill")));
44   m_ui.m_btnTest->setIcon(qApp->icons()->fromTheme(QSL("media-playback-start")));
45   m_ui.m_btnRunOnMessages->setIcon(qApp->icons()->fromTheme(QSL("media-playback-start")));
46   m_ui.m_btnDetailedHelp->setIcon(qApp->icons()->fromTheme(QSL("help-contents")));
47   m_ui.m_txtScript->setFont(QFontDatabase::systemFont(QFontDatabase::SystemFont::FixedFont));
48   m_ui.m_treeExistingMessages->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
49 
50   m_ui.m_treeExistingMessages->header()->setSectionResizeMode(MFM_MODEL_ISREAD, QHeaderView::ResizeMode::ResizeToContents);
51   m_ui.m_treeExistingMessages->header()->setSectionResizeMode(MFM_MODEL_ISIMPORTANT, QHeaderView::ResizeMode::ResizeToContents);
52   m_ui.m_treeExistingMessages->header()->setSectionResizeMode(MFM_MODEL_ISDELETED, QHeaderView::ResizeMode::ResizeToContents);
53   m_ui.m_treeExistingMessages->header()->setSectionResizeMode(MFM_MODEL_AUTHOR, QHeaderView::ResizeMode::ResizeToContents);
54   m_ui.m_treeExistingMessages->header()->setSectionResizeMode(MFM_MODEL_CREATED, QHeaderView::ResizeMode::ResizeToContents);
55   m_ui.m_treeExistingMessages->header()->setSectionResizeMode(MFM_MODEL_SCORE, QHeaderView::ResizeMode::ResizeToContents);
56   m_ui.m_treeExistingMessages->header()->setSectionResizeMode(MFM_MODEL_TITLE, QHeaderView::ResizeMode::Interactive);
57   m_ui.m_treeExistingMessages->header()->setSectionResizeMode(MFM_MODEL_URL, QHeaderView::ResizeMode::Interactive);
58 
59   connect(m_ui.m_btnDetailedHelp, &QPushButton::clicked, this, []() {
60     qApp->web()->openUrlInExternalBrowser(QSL(MSG_FILTERING_HELP));
61   });
62   connect(m_ui.m_listFilters, &QListWidget::currentRowChanged,
63           this, &FormMessageFiltersManager::loadFilter);
64   connect(m_ui.m_btnAddNew, &QPushButton::clicked, this, [this]() {
65     addNewFilter();
66   });
67   connect(m_ui.m_btnRemoveSelected, &QPushButton::clicked,
68           this, &FormMessageFiltersManager::removeSelectedFilter);
69   connect(m_ui.m_txtTitle, &QLineEdit::textChanged, this, &FormMessageFiltersManager::saveSelectedFilter);
70   connect(m_ui.m_txtScript, &QPlainTextEdit::textChanged, this, &FormMessageFiltersManager::saveSelectedFilter);
71   connect(m_ui.m_btnTest, &QPushButton::clicked, this, &FormMessageFiltersManager::testFilter);
72   connect(m_ui.m_btnBeautify, &QPushButton::clicked, this, &FormMessageFiltersManager::beautifyScript);
73   connect(m_ui.m_cmbAccounts, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
74           this,
75           &FormMessageFiltersManager::onAccountChanged);
76   connect(m_ui.m_btnCheckAll, &QPushButton::clicked, m_feedsModel->sourceModel(), &AccountCheckModel::checkAllItems);
77   connect(m_ui.m_btnUncheckAll, &QPushButton::clicked, m_feedsModel->sourceModel(), &AccountCheckModel::uncheckAllItems);
78   connect(m_feedsModel->sourceModel(), &AccountCheckModel::checkStateChanged,
79           this, &FormMessageFiltersManager::onFeedChecked);
80   connect(m_ui.m_treeFeeds->selectionModel(), &QItemSelectionModel::selectionChanged,
81           this, &FormMessageFiltersManager::displayMessagesOfFeed);
82   connect(m_ui.m_btnRunOnMessages, &QPushButton::clicked,
83           this, &FormMessageFiltersManager::processCheckedFeeds);
84   connect(m_ui.m_treeExistingMessages, &QTreeView::customContextMenuRequested,
85           this, &FormMessageFiltersManager::showMessageContextMenu);
86 
87   initializeTestingMessage();
88   loadFilters();
89   loadFilter();
90   loadAccounts();
91 }
92 
~FormMessageFiltersManager()93 FormMessageFiltersManager::~FormMessageFiltersManager() {
94   delete m_rootItem;
95 }
96 
selectedFilter() const97 MessageFilter* FormMessageFiltersManager::selectedFilter() const {
98   if (m_ui.m_listFilters->currentItem() == nullptr) {
99     return nullptr;
100   }
101   else {
102     return m_ui.m_listFilters->currentItem()->data(Qt::ItemDataRole::UserRole).value<MessageFilter*>();
103   }
104 }
105 
selectedAccount() const106 ServiceRoot* FormMessageFiltersManager::selectedAccount() const {
107   auto dat = m_ui.m_cmbAccounts->currentData(Qt::ItemDataRole::UserRole);
108 
109   return dat.isNull() ? nullptr : dat.value<ServiceRoot*>();
110 }
111 
filterMessagesLikeThis(const Message & msg)112 void FormMessageFiltersManager::filterMessagesLikeThis(const Message& msg) {
113   QString filter_script = QSL("function filterMessage() {\n"
114                               "  // Adjust the condition to suit your needs.\n"
115                               "  var is_message_same =\n"
116                               "    msg.isRead == %1 &&\n"
117                               "    msg.isImportant == %2 &&\n"
118                               "    msg.title == '%3' &&\n"
119                               "    msg.url == '%4';\n"
120                               "\n"
121                               "  if (is_message_same) {\n"
122                               "    return MessageObject.Accept;\n"
123                               "  }\n"
124                               "  else {\n"
125                               "    return MessageObject.Ignore;\n"
126                               "  }\n"
127                               "}").arg(QString::number(int(msg.m_isRead)),
128                                        QString::number(int(msg.m_isImportant)),
129                                        msg.m_title,
130                                        msg.m_url);
131 
132   addNewFilter(filter_script);
133 }
134 
showMessageContextMenu(QPoint pos)135 void FormMessageFiltersManager::showMessageContextMenu(QPoint pos) {
136   Message* msg = m_msgModel->messageForRow(m_ui.m_treeExistingMessages->indexAt(pos).row());
137 
138   if (msg != nullptr) {
139     QMenu menu(tr("Context menu"), m_ui.m_treeExistingMessages);
140 
141     menu.addAction(tr("Filter articles like this"), this, [=]() {
142       filterMessagesLikeThis(*msg);
143     });
144     menu.exec(m_ui.m_treeExistingMessages->mapToGlobal(pos));
145   }
146 }
147 
removeSelectedFilter()148 void FormMessageFiltersManager::removeSelectedFilter() {
149   auto* fltr = selectedFilter();
150 
151   if (fltr == nullptr) {
152     return;
153   }
154 
155   m_reader->removeMessageFilter(fltr);
156   delete m_ui.m_listFilters->currentItem();
157 }
158 
loadFilters()159 void FormMessageFiltersManager::loadFilters() {
160   auto flt = m_reader->messageFilters();
161 
162   for (auto* fltr : qAsConst(flt)) {
163     auto* it = new QListWidgetItem(fltr->name(), m_ui.m_listFilters);
164 
165     it->setData(Qt::ItemDataRole::UserRole, QVariant::fromValue<MessageFilter*>(fltr));
166   }
167 }
168 
addNewFilter(const QString & filter_script)169 void FormMessageFiltersManager::addNewFilter(const QString& filter_script) {
170   try {
171     auto* fltr = m_reader->addMessageFilter(
172       tr("New article filter"),
173       filter_script.isEmpty()
174                    ? QSL("function filterMessage() { return MessageObject.Accept; }")
175                    : filter_script);
176     auto* it = new QListWidgetItem(fltr->name(), m_ui.m_listFilters);
177 
178     it->setData(Qt::ItemDataRole::UserRole, QVariant::fromValue<MessageFilter*>(fltr));
179 
180     m_ui.m_listFilters->setCurrentRow(m_ui.m_listFilters->count() - 1);
181   }
182   catch (const ApplicationException& ex) {
183     MessageBox::show(this, QMessageBox::Icon::Critical, tr("Error"),
184                      tr("Cannot save new filter, error: '%1'.").arg(ex.message()));
185   }
186 }
187 
saveSelectedFilter()188 void FormMessageFiltersManager::saveSelectedFilter() {
189   if (m_loadingFilter) {
190     return;
191   }
192 
193   auto* fltr = selectedFilter();
194 
195   if (fltr == nullptr || m_ui.m_txtTitle->text().isEmpty() || m_ui.m_txtScript->toPlainText().isEmpty()) {
196     return;
197   }
198 
199   fltr->setName(m_ui.m_txtTitle->text());
200   fltr->setScript(m_ui.m_txtScript->toPlainText());
201   m_ui.m_listFilters->currentItem()->setText(fltr->name());
202 
203   m_reader->updateMessageFilter(fltr);
204 }
205 
loadFilter()206 void FormMessageFiltersManager::loadFilter() {
207   auto* filter = selectedFilter();
208   auto* acc = selectedAccount();
209 
210   loadAccount(acc);
211   showFilter(filter);
212   loadFilterFeedAssignments(filter, acc);
213 }
214 
testFilter()215 void FormMessageFiltersManager::testFilter() {
216   m_ui.m_txtErrors->clear();
217 
218   // Perform per-message filtering.
219   auto* selected_fd_cat = selectedCategoryFeed();
220   QJSEngine filter_engine;
221   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
222   MessageObject msg_obj(&database,
223                         selected_fd_cat->kind() == RootItem::Kind::Feed
224                         ? selected_fd_cat->customId()
225                         : QString::number(NO_PARENT_CATEGORY),
226                         selectedAccount() != nullptr
227                                              ? selectedAccount()->accountId()
228                                              : NO_PARENT_CATEGORY,
229                         selected_fd_cat->getParentServiceRoot()->labelsNode()->labels(),
230                         false);
231   auto* fltr = selectedFilter();
232 
233   MessageFilter::initializeFilteringEngine(filter_engine, &msg_obj);
234 
235   // Test real messages.
236   try {
237     m_msgModel->testFilter(fltr, &filter_engine, &msg_obj);
238   }
239   catch (const FilteringException& ex) {
240     m_ui.m_txtErrors->setTextColor(Qt::GlobalColor::red);
241     m_ui.m_txtErrors->insertPlainText(tr("EXISTING articles filtering error: '%1'.\n").arg(ex.message()));
242 
243     // See output.
244     m_ui.m_twMessages->setCurrentIndex(2);
245   }
246 
247   // Test sample message.
248   Message msg = testingMessage();
249 
250   msg_obj.setMessage(&msg);
251 
252   try {
253     MessageObject::FilteringAction decision = fltr->filterMessage(&filter_engine);
254 
255     m_ui.m_txtErrors->setTextColor(decision == MessageObject::FilteringAction::Accept ? Qt::GlobalColor::darkGreen : Qt::GlobalColor::red);
256 
257     QString answer = tr("Article will be %1.\n\n").arg(decision == MessageObject::FilteringAction::Accept
258                                                        ? tr("ACCEPTED")
259                                                        : tr("REJECTED"));
260 
261     answer += tr("Output (modified) article is:\n"
262                  "  Title = '%1'\n"
263                  "  URL = '%2'\n"
264                  "  Author = '%3'\n"
265                  "  Is read/important = '%4/%5'\n"
266                  "  Created on = '%6'\n"
267                  "  Contents = '%7'\n"
268                  "  RAW contents = '%8'").arg(msg.m_title, msg.m_url, msg.m_author,
269                                               msg.m_isRead ? tr("yes") : tr("no"),
270                                               msg.m_isImportant ? tr("yes") : tr("no"),
271                                               QString::number(msg.m_created.toMSecsSinceEpoch()),
272                                               msg.m_contents,
273                                               msg.m_rawContents);
274 
275     m_ui.m_txtErrors->insertPlainText(answer);
276   }
277   catch (const FilteringException& ex) {
278     m_ui.m_txtErrors->setTextColor(Qt::GlobalColor::red);
279     m_ui.m_txtErrors->insertPlainText(tr("SAMPLE article filtering error: '%1'.\n").arg(ex.message()));
280 
281     // See output.
282     m_ui.m_twMessages->setCurrentIndex(2);
283   }
284 }
285 
displayMessagesOfFeed()286 void FormMessageFiltersManager::displayMessagesOfFeed() {
287   auto* item = selectedCategoryFeed();
288 
289   if (item != nullptr) {
290     m_msgModel->setMessages(item->undeletedMessages());
291   }
292   else {
293     m_msgModel->setMessages({});
294   }
295 }
296 
processCheckedFeeds()297 void FormMessageFiltersManager::processCheckedFeeds() {
298   QList<RootItem*> checked = m_feedsModel->sourceModel()->checkedItems();
299   auto* fltr = selectedFilter();
300   QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
301 
302   for (RootItem* it : checked) {
303     if (it->kind() == RootItem::Kind::Feed) {
304       QJSEngine filter_engine;
305       MessageObject msg_obj(&database,
306                             it->customId(),
307                             selectedAccount()->accountId(),
308                             it->getParentServiceRoot()->labelsNode()->labels(),
309                             false);
310 
311       MessageFilter::initializeFilteringEngine(filter_engine, &msg_obj);
312 
313       // We process messages of the feed.
314       QList<Message> msgs = it->undeletedMessages();
315       QList<Message> read_msgs, important_msgs;
316 
317       for (int i = 0; i < msgs.size(); i++) {
318         auto labels_in_message = DatabaseQueries::getLabelsForMessage(database, msgs[i], msg_obj.availableLabels());
319 
320         // Create backup of message.
321         Message* msg = &msgs[i]; msg->m_assignedLabels = labels_in_message;
322 
323         msg->m_rawContents = Message::generateRawAtomContents(*msg);
324 
325         Message msg_backup(*msg);
326 
327         msg_obj.setMessage(msg);
328 
329         bool remove_from_list = false;
330 
331         try {
332           MessageObject::FilteringAction result = fltr->filterMessage(&filter_engine);
333 
334           if (result == MessageObject::FilteringAction::Purge) {
335             remove_from_list = true;
336 
337             // Purge the message completely and remove leftovers.
338             DatabaseQueries::purgeMessage(database, msg->m_id);
339             DatabaseQueries::purgeLeftoverLabelAssignments(database, msg->m_accountId);
340           }
341           else if (result == MessageObject::FilteringAction::Ignore) {
342             remove_from_list = true;
343           }
344         }
345         catch (const FilteringException& ex) {
346           qCriticalNN << LOGSEC_CORE
347                       << "Error when running script when processing existing messages:"
348                       << QUOTE_W_SPACE_DOT(ex.message());
349 
350           continue;
351         }
352 
353         if (!msg_backup.m_isRead && msg->m_isRead) {
354           qDebugNN << LOGSEC_FEEDDOWNLOADER << "Message with custom ID: '" << msg_backup.m_customId << "' was marked as read by message scripts.";
355 
356           read_msgs << *msg;
357         }
358 
359         if (!msg_backup.m_isImportant && msg->m_isImportant) {
360           qDebugNN << LOGSEC_FEEDDOWNLOADER << "Message with custom ID: '" << msg_backup.m_customId << "' was marked as important by message scripts.";
361 
362           important_msgs << *msg;
363         }
364 
365         // Process changed labels.
366         for (Label* lbl : qAsConst(msg_backup.m_assignedLabels)) {
367           if (!msg->m_assignedLabels.contains(lbl)) {
368             // Label is not there anymore, it was deassigned.
369             lbl->deassignFromMessage(*msg);
370 
371             qDebugNN << LOGSEC_FEEDDOWNLOADER
372                      << "It was detected that label" << QUOTE_W_SPACE(lbl->customId())
373                      << "was DEASSIGNED from message" << QUOTE_W_SPACE(msg->m_customId)
374                      << "by message filter(s).";
375           }
376         }
377 
378         for (Label* lbl : qAsConst(msg->m_assignedLabels)) {
379           if (!msg_backup.m_assignedLabels.contains(lbl)) {
380             // Label is in new message, but is not in old message, it
381             // was newly assigned.
382             lbl->assignToMessage(*msg);
383 
384             qDebugNN << LOGSEC_FEEDDOWNLOADER
385                      << "It was detected that label" << QUOTE_W_SPACE(lbl->customId())
386                      << "was ASSIGNED to message" << QUOTE_W_SPACE(msg->m_customId)
387                      << "by message filter(s).";
388           }
389         }
390 
391         if (remove_from_list) {
392           // Do not update message.
393           msgs.removeAt(i--);
394         }
395       }
396 
397       if (!read_msgs.isEmpty()) {
398         // Now we push new read states to the service.
399         if (it->getParentServiceRoot()->onBeforeSetMessagesRead(it, read_msgs, RootItem::ReadStatus::Read)) {
400           qDebugNN << LOGSEC_FEEDDOWNLOADER
401                    << "Notified services about messages marked as read by message filters.";
402         }
403         else {
404           qCriticalNN << LOGSEC_FEEDDOWNLOADER
405                       << "Notification of services about messages marked as read by message filters FAILED.";
406         }
407       }
408 
409       if (!important_msgs.isEmpty()) {
410         // Now we push new read states to the service.
411         auto list = boolinq::from(important_msgs).select([](const Message& msg) {
412           return ImportanceChange(msg, RootItem::Importance::Important);
413         }).toStdList();
414         QList<ImportanceChange> chngs = FROM_STD_LIST(QList<ImportanceChange>, list);
415 
416         if (it->getParentServiceRoot()->onBeforeSwitchMessageImportance(it, chngs)) {
417           qDebugNN << LOGSEC_FEEDDOWNLOADER
418                    << "Notified services about messages marked as important by message filters.";
419         }
420         else {
421           qCriticalNN << LOGSEC_FEEDDOWNLOADER
422                       << "Notification of services about messages marked as important by message filters FAILED.";
423         }
424       }
425 
426       // Update messages in DB and reload selection.
427       it->getParentServiceRoot()->updateMessages(msgs, it->toFeed(), true);
428       displayMessagesOfFeed();
429     }
430   }
431 }
432 
loadAccount(ServiceRoot * account)433 void FormMessageFiltersManager::loadAccount(ServiceRoot* account) {
434   m_feedsModel->setRootItem(account, false, true);
435 
436   if (account != nullptr) {
437     m_msgModel->setMessages(account->undeletedMessages());
438   }
439   else {
440     m_msgModel->setMessages({});
441   }
442 }
443 
loadFilterFeedAssignments(MessageFilter * filter,ServiceRoot * account)444 void FormMessageFiltersManager::loadFilterFeedAssignments(MessageFilter* filter, ServiceRoot* account) {
445   if (account == nullptr || filter == nullptr) {
446     return;
447   }
448 
449   m_loadingFilter = true;
450   auto stf = account->getSubTreeFeeds();
451 
452   for (auto* feed : qAsConst(stf)) {
453     if (feed->messageFilters().contains(filter)) {
454       m_feedsModel->sourceModel()->setItemChecked(feed, Qt::CheckState::Checked);
455     }
456   }
457 
458   m_loadingFilter = false;
459 }
460 
onAccountChanged()461 void FormMessageFiltersManager::onAccountChanged() {
462   // Load feeds/categories of the account and check marks.
463   auto* filter = selectedFilter();
464   auto* acc = selectedAccount();
465 
466   loadAccount(acc);
467   loadFilterFeedAssignments(filter, acc);
468 }
469 
onFeedChecked(RootItem * item,Qt::CheckState state)470 void FormMessageFiltersManager::onFeedChecked(RootItem* item, Qt::CheckState state) {
471   if (m_loadingFilter) {
472     return;
473   }
474 
475   auto* feed = qobject_cast<Feed*>(item);
476 
477   if (feed == nullptr) {
478     return;
479   }
480 
481   // Update feed/filter assignemnts.
482   switch (state) {
483     case Qt::CheckState::Checked:
484       m_reader->assignMessageFilterToFeed(feed, selectedFilter());
485       break;
486 
487     case Qt::CheckState::Unchecked:
488       m_reader->removeMessageFilterToFeedAssignment(feed, selectedFilter());
489       break;
490 
491     case Qt::CheckState::PartiallyChecked:
492       break;
493   }
494 }
495 
showFilter(MessageFilter * filter)496 void FormMessageFiltersManager::showFilter(MessageFilter* filter) {
497   m_loadingFilter = true;
498 
499   if (filter == nullptr) {
500     m_ui.m_txtTitle->clear();
501     m_ui.m_txtScript->clear();
502     m_ui.m_gbDetails->setEnabled(false);
503 
504     m_ui.m_treeFeeds->setEnabled(false);
505     m_ui.m_btnCheckAll->setEnabled(false);
506     m_ui.m_btnUncheckAll->setEnabled(false);
507     m_ui.m_cmbAccounts->setEnabled(false);
508   }
509   else {
510     m_ui.m_txtTitle->setText(filter->name());
511     m_ui.m_txtScript->setPlainText(filter->script());
512     m_ui.m_gbDetails->setEnabled(true);
513 
514     m_ui.m_treeFeeds->setEnabled(true);
515     m_ui.m_btnCheckAll->setEnabled(true);
516     m_ui.m_btnUncheckAll->setEnabled(true);
517     m_ui.m_cmbAccounts->setEnabled(true);
518   }
519 
520   // See message.
521   m_ui.m_twMessages->setCurrentIndex(0);
522   m_loadingFilter = false;
523 }
524 
loadAccounts()525 void FormMessageFiltersManager::loadAccounts() {
526   for (auto* acc : qAsConst(m_accounts)) {
527     m_ui.m_cmbAccounts->addItem(acc->icon(), acc->title(), QVariant::fromValue(acc));
528   }
529 }
530 
beautifyScript()531 void FormMessageFiltersManager::beautifyScript() {
532   QProcess proc_clang_format(this);
533 
534   proc_clang_format.setInputChannelMode(QProcess::InputChannelMode::ManagedInputChannel);
535   proc_clang_format.setArguments({ "--assume-filename=script.js", "--style=Chromium" });
536 
537 #if defined(Q_OS_WIN)
538   proc_clang_format.setProgram(qApp->applicationDirPath() + QDir::separator() +
539                                QSL("clang-format") + QDir::separator() +
540                                QSL("clang-format.exe"));
541 #else
542   proc_clang_format.setProgram(QSL("clang-format"));
543 #endif
544 
545   if (!proc_clang_format.open() || proc_clang_format.error() == QProcess::ProcessError::FailedToStart) {
546     MessageBox::show(this, QMessageBox::Icon::Critical,
547                      tr("Cannot find 'clang-format'"),
548                      tr("Script was not beautified, because 'clang-format' tool was not found."));
549     return;
550   }
551 
552   proc_clang_format.write(m_ui.m_txtScript->toPlainText().toUtf8());
553   proc_clang_format.closeWriteChannel();
554 
555   if (proc_clang_format.waitForFinished(3000)) {
556     if (proc_clang_format.exitCode() == 0) {
557       auto script = proc_clang_format.readAllStandardOutput();
558 
559       m_ui.m_txtScript->setPlainText(script);
560     }
561     else {
562       auto err = proc_clang_format.readAllStandardError();
563 
564       MessageBox::show(this, QMessageBox::Icon::Critical,
565                        tr("Error"),
566                        tr("Script was not beautified, because 'clang-format' tool thrown error."),
567                        QString(),
568                        err);
569     }
570   }
571   else {
572     proc_clang_format.kill();
573     MessageBox::show(this, QMessageBox::Icon::Critical,
574                      tr("Beautifier was running for too long time"),
575                      tr("Script was not beautified, is 'clang-format' installed?"));
576   }
577 }
578 
initializeTestingMessage()579 void FormMessageFiltersManager::initializeTestingMessage() {
580   m_ui.m_cbSampleImportant->setChecked(true);
581   m_ui.m_txtSampleUrl->setText(QSL("https://mynews.com/news/5"));
582   m_ui.m_txtSampleTitle->setText(QSL("Year of Linux Desktop"));
583   m_ui.m_txtSampleAuthor->setText(QSL("Napoleon Bonaparte"));
584   m_ui.m_txtSampleContents->setPlainText(QSL("<p>Browsers usually insert quotation marks around the q element.</p>"
585                                              "<p>WWF's goal is to: <q>Build a future where people live in harmony "
586                                              "with nature.</q></p>"));
587   m_ui.m_txtSampleCreatedOn->setText(QString::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()));
588 }
589 
selectedCategoryFeed() const590 RootItem* FormMessageFiltersManager::selectedCategoryFeed() const {
591   return m_feedsModel->sourceModel()->itemForIndex(m_feedsModel->mapToSource(m_ui.m_treeFeeds->currentIndex()));
592 }
593 
testingMessage() const594 Message FormMessageFiltersManager::testingMessage() const {
595   Message msg;
596 
597   msg.m_feedId = NO_PARENT_CATEGORY;
598   msg.m_url = m_ui.m_txtSampleUrl->text();
599   msg.m_customId = m_ui.m_txtSampleUrl->text();
600   msg.m_title = m_ui.m_txtSampleTitle->text();
601   msg.m_author = m_ui.m_txtSampleAuthor->text();
602   msg.m_isRead = m_ui.m_cbSampleRead->isChecked();
603   msg.m_isImportant = m_ui.m_cbSampleImportant->isChecked();
604   msg.m_created = QDateTime::fromMSecsSinceEpoch(m_ui.m_txtSampleCreatedOn->text().toLongLong());
605   msg.m_contents = m_ui.m_txtSampleContents->toPlainText();
606   msg.m_rawContents = Message::generateRawAtomContents(msg);
607 
608   return msg;
609 }
610