1 #include "releasechannel.h"
2
3 #include "version_string.h"
4
5 #include <QJsonArray>
6 #include <QJsonDocument>
7 #include <QJsonObject>
8 #include <QMessageBox>
9 #include <QNetworkReply>
10 #include <QRegularExpression>
11 #include <QSysInfo>
12 #include <QtGlobal>
13
14 #define STABLERELEASE_URL "https://api.github.com/repos/Cockatrice/Cockatrice/releases/latest"
15 #define STABLEMANUALDOWNLOAD_URL "https://github.com/Cockatrice/Cockatrice/releases/latest"
16 #define STABLETAG_URL "https://api.github.com/repos/Cockatrice/Cockatrice/git/refs/tags/"
17
18 #define BETARELEASE_URL "https://api.github.com/repos/Cockatrice/Cockatrice/releases"
19 #define BETAMANUALDOWNLOAD_URL "https://github.com/Cockatrice/Cockatrice/releases/"
20 #define BETARELEASE_CHANGESURL "https://github.com/Cockatrice/Cockatrice/compare/%1...%2"
21
22 #define GIT_SHORT_HASH_LEN 7
23
24 int ReleaseChannel::sharedIndex = 0;
25
ReleaseChannel()26 ReleaseChannel::ReleaseChannel() : response(nullptr), lastRelease(nullptr)
27 {
28 index = sharedIndex++;
29 netMan = new QNetworkAccessManager(this);
30 }
31
~ReleaseChannel()32 ReleaseChannel::~ReleaseChannel()
33 {
34 netMan->deleteLater();
35 }
36
checkForUpdates()37 void ReleaseChannel::checkForUpdates()
38 {
39 QString releaseChannelUrl = getReleaseChannelUrl();
40 qDebug() << "Searching for updates on the channel: " << releaseChannelUrl;
41 response = netMan->get(QNetworkRequest(releaseChannelUrl));
42 connect(response, SIGNAL(finished()), this, SLOT(releaseListFinished()));
43 }
44
45 // Different release channel checking functions for different operating systems
46 #if defined(Q_OS_OSX)
downloadMatchesCurrentOS(const QString & fileName)47 bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
48 {
49 static QRegularExpression version_regex("macOS-(\\d+)\\.(\\d+)");
50 auto match = version_regex.match(fileName);
51 if (!match.hasMatch()) {
52 return false;
53 }
54
55 // older(smaller) releases are compatible with a newer or the same system version
56 int sys_maj = QSysInfo::productVersion().split(".")[0].toInt();
57 int sys_min = QSysInfo::productVersion().split(".")[1].toInt();
58 int rel_maj = match.captured(1).toInt();
59 int rel_min = match.captured(2).toInt();
60 return rel_maj < sys_maj || (rel_maj == sys_maj && rel_min <= rel_min);
61 }
62 #elif defined(Q_OS_WIN)
downloadMatchesCurrentOS(const QString & fileName)63 bool ReleaseChannel::downloadMatchesCurrentOS(const QString &fileName)
64 {
65 #if Q_PROCESSOR_WORDSIZE == 4
66 return fileName.contains("win32");
67 #elif Q_PROCESSOR_WORDSIZE == 8
68 return fileName.contains("win64");
69 #else
70 return false;
71 #endif
72 }
73 #else
downloadMatchesCurrentOS(const QString &)74 bool ReleaseChannel::downloadMatchesCurrentOS(const QString &)
75 {
76 // If the OS doesn't fit one of the above #defines, then it will never match
77 return false;
78 }
79 #endif
80
getManualDownloadUrl() const81 QString StableReleaseChannel::getManualDownloadUrl() const
82 {
83 return QString(STABLEMANUALDOWNLOAD_URL);
84 }
85
getName() const86 QString StableReleaseChannel::getName() const
87 {
88 return tr("Stable Releases");
89 }
90
getReleaseChannelUrl() const91 QString StableReleaseChannel::getReleaseChannelUrl() const
92 {
93 return QString(STABLERELEASE_URL);
94 }
95
releaseListFinished()96 void StableReleaseChannel::releaseListFinished()
97 {
98 auto *reply = static_cast<QNetworkReply *>(sender());
99 QJsonParseError parseError{};
100 QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
101 reply->deleteLater();
102 if (parseError.error != QJsonParseError::NoError) {
103 qWarning() << "No reply received from the release update server.";
104 emit error(tr("No reply received from the release update server."));
105 return;
106 }
107
108 QVariantMap resultMap = jsonResponse.toVariant().toMap();
109 if (!(resultMap.contains("name") && resultMap.contains("html_url") && resultMap.contains("tag_name") &&
110 resultMap.contains("published_at"))) {
111 qWarning() << "Invalid received from the release update server.";
112 emit error(tr("Invalid reply received from the release update server."));
113 return;
114 }
115
116 if (!lastRelease)
117 lastRelease = new Release;
118
119 lastRelease->setName(resultMap["name"].toString());
120 lastRelease->setDescriptionUrl(resultMap["html_url"].toString());
121 lastRelease->setPublishDate(resultMap["published_at"].toDate());
122
123 if (resultMap.contains("assets")) {
124 auto rawAssets = resultMap["assets"].toList();
125 // [(name, url)]
126 QVector<std::pair<QString, QString>> assets;
127 std::transform(rawAssets.begin(), rawAssets.end(), std::back_inserter(assets), [](QVariant _asset) {
128 QVariantMap asset = _asset.toMap();
129 QString name = asset["name"].toString();
130 QString url = asset["browser_download_url"].toString();
131 return std::make_pair(name, url);
132 });
133
134 auto _releaseAsset = std::find_if(assets.begin(), assets.end(), [](std::pair<QString, QString> nameAndUrl) {
135 return downloadMatchesCurrentOS(nameAndUrl.first);
136 });
137
138 if (_releaseAsset != assets.end()) {
139 std::pair<QString, QString> releaseAsset = *_releaseAsset;
140 auto releaseUrl = releaseAsset.second;
141 lastRelease->setDownloadUrl(releaseUrl);
142 }
143 }
144
145 QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
146 QString myHash = QString(VERSION_COMMIT);
147 qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
148
149 qDebug() << "Got reply from release server, name=" << lastRelease->getName()
150 << "desc=" << lastRelease->getDescriptionUrl() << "date=" << lastRelease->getPublishDate()
151 << "url=" << lastRelease->getDownloadUrl();
152
153 const QString &tagName = resultMap["tag_name"].toString();
154 QString url = QString(STABLETAG_URL) + tagName;
155 qDebug() << "Searching for commit hash corresponding to stable channel tag: " << tagName;
156 response = netMan->get(QNetworkRequest(url));
157 connect(response, SIGNAL(finished()), this, SLOT(tagListFinished()));
158 }
159
tagListFinished()160 void StableReleaseChannel::tagListFinished()
161 {
162 auto *reply = static_cast<QNetworkReply *>(sender());
163 QJsonParseError parseError{};
164 QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
165 reply->deleteLater();
166 if (parseError.error != QJsonParseError::NoError) {
167 qWarning() << "No reply received from the tag update server.";
168 emit error(tr("No reply received from the tag update server."));
169 return;
170 }
171
172 QVariantMap resultMap = jsonResponse.toVariant().toMap();
173 if (!(resultMap.contains("object") && resultMap["object"].toMap().contains("sha"))) {
174 qWarning() << "Invalid received from the tag update server.";
175 emit error(tr("Invalid reply received from the tag update server."));
176 return;
177 }
178
179 lastRelease->setCommitHash(resultMap["object"].toMap()["sha"].toString());
180 qDebug() << "Got reply from tag server, commit=" << lastRelease->getCommitHash();
181
182 QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
183 QString myHash = QString(VERSION_COMMIT);
184 qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
185 const bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
186
187 emit finishedCheck(needToUpdate, lastRelease->isCompatibleVersionFound(), lastRelease);
188 }
189
fileListFinished()190 void StableReleaseChannel::fileListFinished()
191 {
192 // Only implemented to satisfy interface
193 }
194
getManualDownloadUrl() const195 QString BetaReleaseChannel::getManualDownloadUrl() const
196 {
197 return QString(BETAMANUALDOWNLOAD_URL);
198 }
199
getName() const200 QString BetaReleaseChannel::getName() const
201 {
202 return tr("Beta Releases");
203 }
204
getReleaseChannelUrl() const205 QString BetaReleaseChannel::getReleaseChannelUrl() const
206 {
207 return QString(BETARELEASE_URL);
208 }
209
releaseListFinished()210 void BetaReleaseChannel::releaseListFinished()
211 {
212 auto *reply = static_cast<QNetworkReply *>(sender());
213 QByteArray jsonData = reply->readAll();
214 reply->deleteLater();
215
216 QJsonDocument doc = QJsonDocument::fromJson(jsonData);
217 QJsonArray array = doc.array();
218
219 /*
220 * Get the latest release on GitHub
221 * This can be either a pre-release or a published release
222 * depending on timing. Both are acceptable.
223 */
224 QVariantMap resultMap = array.at(0).toObject().toVariantMap();
225
226 if (array.empty() || resultMap.empty()) {
227 qWarning() << "No reply received from the release update server:" << QString(jsonData);
228 emit error(tr("No reply received from the release update server."));
229 return;
230 }
231
232 // Make sure resultMap has all elements we'll need
233 if (!resultMap.contains("assets") || !resultMap.contains("author") || !resultMap.contains("tag_name") ||
234 !resultMap.contains("target_commitish") || !resultMap.contains("assets_url") ||
235 !resultMap.contains("published_at")) {
236 qWarning() << "Invalid received from the release update server:" << resultMap;
237 emit error(tr("Invalid reply received from the release update server."));
238 return;
239 }
240
241 if (lastRelease == nullptr)
242 lastRelease = new Release;
243
244 lastRelease->setCommitHash(resultMap["target_commitish"].toString());
245 lastRelease->setPublishDate(resultMap["published_at"].toDate());
246
247 QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
248 lastRelease->setName(QString("%1 (%2)").arg(resultMap["tag_name"].toString()).arg(shortHash));
249 lastRelease->setDescriptionUrl(QString(BETARELEASE_CHANGESURL).arg(VERSION_COMMIT, shortHash));
250
251 qDebug() << "Got reply from release server, size=" << resultMap.size() << "name=" << lastRelease->getName()
252 << "desc=" << lastRelease->getDescriptionUrl() << "commit=" << lastRelease->getCommitHash()
253 << "date=" << lastRelease->getPublishDate();
254
255 QString betaBuildDownloadUrl = resultMap["assets_url"].toString();
256
257 qDebug() << "Searching for a corresponding file on the beta channel: " << betaBuildDownloadUrl;
258 response = netMan->get(QNetworkRequest(betaBuildDownloadUrl));
259 connect(response, SIGNAL(finished()), this, SLOT(fileListFinished()));
260 }
261
fileListFinished()262 void BetaReleaseChannel::fileListFinished()
263 {
264 auto *reply = static_cast<QNetworkReply *>(sender());
265 QJsonParseError parseError{};
266 QJsonDocument jsonResponse = QJsonDocument::fromJson(reply->readAll(), &parseError);
267 reply->deleteLater();
268 if (parseError.error != QJsonParseError::NoError) {
269 qWarning() << "No reply received from the file update server.";
270 emit error(tr("No reply received from the file update server."));
271 return;
272 }
273
274 QVariantList resultList = jsonResponse.toVariant().toList();
275 QString shortHash = lastRelease->getCommitHash().left(GIT_SHORT_HASH_LEN);
276 QString myHash = QString(VERSION_COMMIT);
277 qDebug() << "Current hash=" << myHash << "update hash=" << shortHash;
278
279 bool needToUpdate = (QString::compare(shortHash, myHash, Qt::CaseInsensitive) != 0);
280 bool compatibleVersion = false;
281
282 QStringList resultUrlList{};
283 for (QVariant file : resultList) {
284 QVariantMap map = file.toMap();
285 resultUrlList << map["browser_download_url"].toString();
286 }
287
288 resultUrlList.sort();
289 // iterate in reverse so the first item is the latest os version
290 for (auto url = resultUrlList.rbegin(); url < resultUrlList.rend(); ++url) {
291 if (downloadMatchesCurrentOS(*url)) {
292 compatibleVersion = true;
293 lastRelease->setDownloadUrl(*url);
294 qDebug() << "Found compatible version url=" << *url;
295 break;
296 }
297 }
298
299 emit finishedCheck(needToUpdate, compatibleVersion, lastRelease);
300 }
301