1 #include "spoilerbackgroundupdater.h"
2 
3 #include "carddatabase.h"
4 #include "main.h"
5 #include "settingscache.h"
6 #include "window_main.h"
7 
8 #include <QApplication>
9 #include <QCryptographicHash>
10 #include <QDateTime>
11 #include <QDebug>
12 #include <QFile>
13 #include <QLocale>
14 #include <QMessageBox>
15 #include <QNetworkReply>
16 #include <QUrl>
17 #include <QtConcurrent>
18 
19 #define SPOILERS_STATUS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/SpoilerSeasonEnabled"
20 #define SPOILERS_URL "https://raw.githubusercontent.com/Cockatrice/Magic-Spoiler/files/spoiler.xml"
21 
SpoilerBackgroundUpdater(QObject * apParent)22 SpoilerBackgroundUpdater::SpoilerBackgroundUpdater(QObject *apParent) : QObject(apParent), cardUpdateProcess(nullptr)
23 {
24     isSpoilerDownloadEnabled = SettingsCache::instance().getDownloadSpoilersStatus();
25     if (isSpoilerDownloadEnabled) {
26         // Start the process of checking if we're in spoiler season
27         // File exists means we're in spoiler season
28         startSpoilerDownloadProcess(SPOILERS_STATUS_URL, false);
29     } else {
30         qDebug() << "Spoilers Disabled";
31     }
32 }
33 
startSpoilerDownloadProcess(QString url,bool saveResults)34 void SpoilerBackgroundUpdater::startSpoilerDownloadProcess(QString url, bool saveResults)
35 {
36     auto spoilerURL = QUrl(url);
37     downloadFromURL(spoilerURL, saveResults);
38 }
39 
downloadFromURL(QUrl url,bool saveResults)40 void SpoilerBackgroundUpdater::downloadFromURL(QUrl url, bool saveResults)
41 {
42     auto *nam = new QNetworkAccessManager(this);
43     QNetworkReply *reply = nam->get(QNetworkRequest(url));
44 
45     if (saveResults) {
46         // This will write out to the file (used for spoiler.xml)
47         connect(reply, SIGNAL(finished()), this, SLOT(actDownloadFinishedSpoilersFile()));
48     } else {
49         // This will check the status (used to see if we're in spoiler season or not)
50         connect(reply, SIGNAL(finished()), this, SLOT(actCheckIfSpoilerSeasonEnabled()));
51     }
52 }
53 
actDownloadFinishedSpoilersFile()54 void SpoilerBackgroundUpdater::actDownloadFinishedSpoilersFile()
55 {
56     // Check for server reply
57     auto *reply = dynamic_cast<QNetworkReply *>(sender());
58     QNetworkReply::NetworkError errorCode = reply->error();
59 
60     if (errorCode == QNetworkReply::NoError) {
61         spoilerData = reply->readAll();
62 
63         // Save the spoiler.xml file to the disk
64         saveDownloadedFile(spoilerData);
65 
66         reply->deleteLater();
67         emit spoilerCheckerDone();
68     } else {
69         qDebug() << "Error downloading spoilers file" << errorCode;
70         emit spoilerCheckerDone();
71     }
72 }
73 
deleteSpoilerFile()74 bool SpoilerBackgroundUpdater::deleteSpoilerFile()
75 {
76     QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath();
77     QFileInfo fi(fileName);
78     QDir fileDir(fi.path());
79     QFile file(fileName);
80 
81     // Delete the spoiler.xml file
82     if (file.exists() && file.remove()) {
83         qDebug() << "Deleting spoiler.xml";
84         return true;
85     }
86 
87     qDebug() << "Error: Spoiler.xml not found or not deleted";
88     return false;
89 }
90 
actCheckIfSpoilerSeasonEnabled()91 void SpoilerBackgroundUpdater::actCheckIfSpoilerSeasonEnabled()
92 {
93     auto *response = dynamic_cast<QNetworkReply *>(sender());
94     QNetworkReply::NetworkError errorCode = response->error();
95 
96     if (errorCode == QNetworkReply::ContentNotFoundError) {
97         // Spoiler season is offline at this point, so the spoiler.xml file can be safely deleted
98         // The user should run Oracle to get the latest card information
99         if (deleteSpoilerFile() && trayIcon) {
100             trayIcon->showMessage(tr("Spoilers season has ended"), tr("Deleting spoiler.xml. Please run Oracle"));
101         }
102 
103         qDebug() << "Spoiler Season Offline";
104         emit spoilerCheckerDone();
105     } else if (errorCode == QNetworkReply::NoError) {
106         qDebug() << "Spoiler Service Online";
107         startSpoilerDownloadProcess(SPOILERS_URL, true);
108     } else if (errorCode == QNetworkReply::HostNotFoundError) {
109         if (trayIcon) {
110             trayIcon->showMessage(tr("Spoilers download failed"), tr("No internet connection"));
111         }
112 
113         qDebug() << "Spoiler download failed due to no internet connection";
114         emit spoilerCheckerDone();
115     } else {
116         if (trayIcon) {
117             trayIcon->showMessage(tr("Spoilers download failed"), tr("Error") + " " + errorCode);
118         }
119 
120         qDebug() << "Spoiler download failed with reason" << errorCode;
121         emit spoilerCheckerDone();
122     }
123 }
124 
saveDownloadedFile(QByteArray data)125 bool SpoilerBackgroundUpdater::saveDownloadedFile(QByteArray data)
126 {
127     QString fileName = SettingsCache::instance().getSpoilerCardDatabasePath();
128     QFileInfo fi(fileName);
129     QDir fileDir(fi.path());
130 
131     if (!fileDir.exists() && !fileDir.mkpath(fileDir.absolutePath())) {
132         return false;
133     }
134 
135     // Check if the data matches. If it does, then spoilers are up to date.
136     if (getHash(fileName) == getHash(data)) {
137         if (trayIcon) {
138             trayIcon->showMessage(tr("Spoilers already up to date"), tr("No new spoilers added"));
139         }
140 
141         qDebug() << "Spoilers Up to Date";
142         return false;
143     }
144 
145     QFile file(fileName);
146     if (!file.open(QIODevice::WriteOnly)) {
147         qDebug() << "Spoiler Service Error: File open (w) failed for" << fileName;
148         file.close();
149         return false;
150     }
151 
152     if (file.write(data) == -1) {
153         qDebug() << "Spoiler Service Error: File write (w) failed for" << fileName;
154         file.close();
155         return false;
156     }
157 
158     file.close();
159 
160     // Data written, so reload the card database
161     qDebug() << "Spoiler Service Data Written";
162     QtConcurrent::run(db, &CardDatabase::loadCardDatabases);
163 
164     // If the user has notifications enabled, let them know
165     // when the database was last updated
166     if (trayIcon) {
167         QList<QByteArray> lines = data.split('\n');
168 
169         foreach (QByteArray line, lines) {
170             if (line.contains("Created At:")) {
171                 QString timeStamp = QString(line).replace("Created At:", "").trimmed();
172                 timeStamp.chop(6); // Remove " (UTC)"
173 
174                 auto utcTime = QLocale().toDateTime(timeStamp, "ddd, MMM dd yyyy, hh:mm:ss");
175                 utcTime.setTimeSpec(Qt::UTC);
176 
177                 QString localTime = utcTime.toLocalTime().toString("MMM d, hh:mm");
178 
179                 trayIcon->showMessage(tr("Spoilers have been updated!"), tr("Last change:") + " " + localTime);
180                 emit spoilersUpdatedSuccessfully();
181                 return true;
182             }
183         }
184     }
185 
186     return true;
187 }
188 
getHash(const QString fileName)189 QByteArray SpoilerBackgroundUpdater::getHash(const QString fileName)
190 {
191     QFile file(fileName);
192 
193     if (file.open(QFile::ReadOnly)) {
194         // Only read the first 512 bytes (enough to get the "created" tag)
195         const QByteArray bytes = file.read(512);
196 
197         QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
198         hash.addData(bytes);
199 
200         qDebug() << "File Hash =" << hash.result();
201 
202         file.close();
203         return hash.result();
204     } else {
205         qDebug() << "getHash ReadOnly failed!";
206         file.close();
207         return QByteArray();
208     }
209 }
210 
getHash(QByteArray data)211 QByteArray SpoilerBackgroundUpdater::getHash(QByteArray data)
212 {
213     // Only read the first 512 bytes (enough to get the "created" tag)
214     const QByteArray bytes = data.left(512);
215 
216     QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
217     hash.addData(bytes);
218 
219     qDebug() << "Data Hash =" << hash.result();
220 
221     return hash.result();
222 }
223