File indexing completed on 2024-05-12 16:23:40

0001 /**
0002  * SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include <KLocalizedString>
0008 #include <QCryptographicHash>
0009 #include <QDateTime>
0010 #include <QDebug>
0011 #include <QFile>
0012 #include <QFileInfo>
0013 #include <QNetworkReply>
0014 #include <QStandardPaths>
0015 #include <QTextDocumentFragment>
0016 #include <Syndication/Syndication>
0017 
0018 #include "database.h"
0019 #include "fetcher.h"
0020 
0021 Fetcher::Fetcher()
0022     : m_fetchCount(0)
0023 {
0024     manager = new QNetworkAccessManager(this);
0025     manager->setRedirectPolicy(QNetworkRequest::NoLessSafeRedirectPolicy);
0026     manager->setStrictTransportSecurityEnabled(true);
0027     manager->enableStrictTransportSecurityStore(true);
0028 }
0029 
0030 void Fetcher::fetch(const QString &url, const bool markEntriesRead)
0031 {
0032     qDebug() << "Starting to fetch" << url;
0033 
0034     Q_EMIT startedFetchingFeed(url);
0035     setFetchCount(m_fetchCount + 1);
0036 
0037     QNetworkRequest request((QUrl(url)));
0038     QNetworkReply *reply = get(request);
0039     connect(reply, &QNetworkReply::finished, this, [this, url, reply, markEntriesRead]() {
0040         setFetchCount(m_fetchCount - 1);
0041         if (reply->error()) {
0042             qWarning() << "Error fetching feed";
0043             qWarning() << reply->errorString();
0044             Q_EMIT error(url, reply->error(), reply->errorString());
0045         } else {
0046             QByteArray data = reply->readAll();
0047             Syndication::DocumentSource document(data, url);
0048             Syndication::FeedPtr feed = Syndication::parserCollection()->parse(document, QStringLiteral("Atom"));
0049             processFeed(feed, url, markEntriesRead);
0050         }
0051         delete reply;
0052     });
0053 }
0054 
0055 void Fetcher::fetchAll()
0056 {
0057     QSqlQuery query;
0058     if (query.prepare(QStringLiteral("SELECT url FROM Feeds;"))) {
0059         Database::instance().execute(query);
0060         while (query.next()) {
0061             fetch(query.value(0).toString());
0062         }
0063     }
0064 }
0065 
0066 void Fetcher::setFetchCount(int count)
0067 {
0068     m_fetchCount = count;
0069     Q_EMIT refreshingChanged(refreshing());
0070 }
0071 
0072 void Fetcher::processFeed(Syndication::FeedPtr feed, const QString &url, const bool markEntriesRead)
0073 {
0074     if (feed.isNull()) {
0075         Syndication::ErrorCode errorCode = Syndication::parserCollection()->lastError();
0076         QString errorString = syndicationErrorToString(errorCode);
0077         Q_EMIT error(url, errorCode, errorString);
0078         return;
0079     }
0080 
0081     QSqlQuery query;
0082     if (query.prepare(
0083             QStringLiteral("UPDATE Feeds SET name=:name, image=:image, link=:link, description=:description, lastUpdated=:lastUpdated WHERE url=:url;"))) {
0084         query.bindValue(QStringLiteral(":name"), feed->title());
0085         query.bindValue(QStringLiteral(":url"), url);
0086         query.bindValue(QStringLiteral(":link"), feed->link());
0087         query.bindValue(QStringLiteral(":description"), feed->description());
0088 
0089         QDateTime current = QDateTime::currentDateTime();
0090         query.bindValue(QStringLiteral(":lastUpdated"), current.toSecsSinceEpoch());
0091 
0092         for (auto &author : feed->authors()) {
0093             processAuthor(author, QLatin1String(""), url);
0094         }
0095 
0096         QString imagePath;
0097         if (feed->image()->url().startsWith(QStringLiteral("/"))) {
0098             imagePath = QUrl(url).adjusted(QUrl::RemovePath).toString() + feed->image()->url();
0099         } else {
0100             imagePath = feed->image()->url();
0101         }
0102         query.bindValue(QStringLiteral(":image"), imagePath);
0103         Database::instance().execute(query);
0104 
0105         qDebug() << "Updated feed title:" << feed->title();
0106 
0107         Q_EMIT feedDetailsUpdated(url, feed->title(), imagePath, feed->link(), feed->description(), current);
0108     }
0109 
0110     for (const auto &entry : feed->items()) {
0111         processEntry(entry, url, markEntriesRead);
0112     }
0113 
0114     Q_EMIT feedUpdated(url);
0115 }
0116 
0117 void Fetcher::processEntry(Syndication::ItemPtr entry, const QString &url, const bool markEntriesRead)
0118 {
0119     qDebug() << "Processing" << entry->title();
0120     QSqlQuery query;
0121     if (query.prepare(QStringLiteral("SELECT COUNT (id) FROM Entries WHERE id=:id;"))) {
0122         query.bindValue(QStringLiteral(":id"), entry->id());
0123         Database::instance().execute(query);
0124         query.next();
0125 
0126         if (query.value(0).toInt() != 0) {
0127             return;
0128         }
0129     }
0130 
0131     if (query.prepare(QStringLiteral("INSERT INTO Entries VALUES (:feed, :id, :title, :content, :created, :updated, :link, :read);"))) {
0132         query.bindValue(QStringLiteral(":feed"), url);
0133         query.bindValue(QStringLiteral(":id"), entry->id());
0134         query.bindValue(QStringLiteral(":title"), QTextDocumentFragment::fromHtml(entry->title()).toPlainText());
0135         query.bindValue(QStringLiteral(":created"), static_cast<int>(entry->datePublished()));
0136         query.bindValue(QStringLiteral(":updated"), static_cast<int>(entry->dateUpdated()));
0137         query.bindValue(QStringLiteral(":link"), entry->link());
0138         query.bindValue(QStringLiteral(":read"), markEntriesRead);
0139 
0140         if (!entry->content().isEmpty()) {
0141             query.bindValue(QStringLiteral(":content"), entry->content());
0142         } else {
0143             query.bindValue(QStringLiteral(":content"), entry->description());
0144         }
0145 
0146         Database::instance().execute(query);
0147     }
0148 
0149     for (const auto &author : entry->authors()) {
0150         processAuthor(author, entry->id(), url);
0151     }
0152 
0153     for (const auto &enclosure : entry->enclosures()) {
0154         processEnclosure(enclosure, entry, url);
0155     }
0156 }
0157 
0158 void Fetcher::processAuthor(Syndication::PersonPtr author, const QString &entryId, const QString &url)
0159 {
0160     QSqlQuery query;
0161     if (query.prepare(QStringLiteral("INSERT INTO Authors VALUES(:feed, :id, :name, :uri, :email);"))) {
0162         query.bindValue(QStringLiteral(":feed"), url);
0163         query.bindValue(QStringLiteral(":id"), entryId);
0164         query.bindValue(QStringLiteral(":name"), author->name());
0165         query.bindValue(QStringLiteral(":uri"), author->uri());
0166         query.bindValue(QStringLiteral(":email"), author->email());
0167         Database::instance().execute(query);
0168     }
0169 }
0170 
0171 void Fetcher::processEnclosure(Syndication::EnclosurePtr enclosure, Syndication::ItemPtr entry, const QString &feedUrl)
0172 {
0173     QSqlQuery query;
0174     if (query.prepare(QStringLiteral("INSERT INTO Enclosures VALUES (:feed, :id, :duration, :size, :title, :type, :url);"))) {
0175         query.bindValue(QStringLiteral(":feed"), feedUrl);
0176         query.bindValue(QStringLiteral(":id"), entry->id());
0177         query.bindValue(QStringLiteral(":duration"), enclosure->duration());
0178         query.bindValue(QStringLiteral(":size"), enclosure->length());
0179         query.bindValue(QStringLiteral(":title"), enclosure->title());
0180         query.bindValue(QStringLiteral(":type"), enclosure->type());
0181         query.bindValue(QStringLiteral(":url"), enclosure->url());
0182         Database::instance().execute(query);
0183     }
0184 }
0185 
0186 QString Fetcher::image(const QString &url)
0187 {
0188     QString path = filePath(url);
0189     if (QFileInfo::exists(path)) {
0190         return path;
0191     }
0192 
0193     download(url);
0194 
0195     return QLatin1String("");
0196 }
0197 
0198 void Fetcher::download(const QString &url)
0199 {
0200     QNetworkRequest request((QUrl(url)));
0201     QNetworkReply *reply = get(request);
0202     connect(reply, &QNetworkReply::finished, this, [this, url, reply]() {
0203         QByteArray data = reply->readAll();
0204         QFile file(filePath(url));
0205         file.open(QIODevice::WriteOnly);
0206         file.write(data);
0207         file.close();
0208 
0209         Q_EMIT imageDownloadFinished(url);
0210         delete reply;
0211     });
0212 }
0213 
0214 void Fetcher::removeImage(const QString &url)
0215 {
0216     qDebug() << filePath(url);
0217     QFile(filePath(url)).remove();
0218 }
0219 
0220 QString Fetcher::filePath(const QString &url)
0221 {
0222     return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/")
0223         + QString::fromStdString(QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Md5).toHex().toStdString());
0224 }
0225 
0226 QNetworkReply *Fetcher::get(QNetworkRequest &request)
0227 {
0228     request.setRawHeader("User-Agent", "Alligator/0.1; Syndication");
0229     return manager->get(request);
0230 }
0231 
0232 QString Fetcher::syndicationErrorToString(Syndication::ErrorCode errorCode)
0233 {
0234     switch (errorCode) {
0235     case Syndication::InvalidXml:
0236         return i18n("Invalid XML");
0237     case Syndication::XmlNotAccepted:
0238         return i18n("No parser accepted the XML");
0239     default:
0240         return i18n("Error while parsing feed");
0241     }
0242 }