2014-11-10 00:55:38 +08:00
|
|
|
/*
|
|
|
|
Copyright (C) 2014 by Project Tox <https://tox.im>
|
|
|
|
|
|
|
|
This file is part of qTox, a Qt-based graphical interface for Tox.
|
|
|
|
|
|
|
|
This program is libre software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
|
|
|
|
|
|
See the COPYING file for more details.
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
2014-11-09 22:51:00 +08:00
|
|
|
#include "src/autoupdate.h"
|
2014-11-10 00:55:38 +08:00
|
|
|
#include "src/misc/serialize.h"
|
2014-11-10 06:24:23 +08:00
|
|
|
#include "src/misc/settings.h"
|
2014-11-10 07:31:29 +08:00
|
|
|
#include "src/widget/widget.h"
|
2014-11-09 22:51:00 +08:00
|
|
|
#include <QNetworkAccessManager>
|
|
|
|
#include <QNetworkReply>
|
|
|
|
#include <QCoreApplication>
|
2014-11-10 06:24:23 +08:00
|
|
|
#include <QFile>
|
|
|
|
#include <QDir>
|
|
|
|
#include <QProcess>
|
2014-11-10 07:31:29 +08:00
|
|
|
#include <QtConcurrent/QtConcurrent>
|
|
|
|
#include <QMessageBox>
|
2014-11-09 22:51:00 +08:00
|
|
|
|
2014-11-10 06:24:23 +08:00
|
|
|
#ifdef Q_OS_WIN
|
|
|
|
#include <windows.h>
|
|
|
|
#include <shellapi.h>
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#ifdef Q_OS_WIN
|
2014-11-09 22:51:00 +08:00
|
|
|
const QString AutoUpdater::platform = "win32";
|
2014-11-10 06:24:23 +08:00
|
|
|
const QString AutoUpdater::updaterBin = "qtox-updater.exe";
|
2014-11-12 05:37:51 +08:00
|
|
|
const QString AutoUpdater::updateServer = "https://s3.amazonaws.com/qtox-updater";
|
|
|
|
|
|
|
|
unsigned char AutoUpdater::key[crypto_sign_PUBLICKEYBYTES] =
|
|
|
|
{
|
|
|
|
0xa5, 0x80, 0xf3, 0xb7, 0xd0, 0x10, 0xc0, 0xf9, 0xd6, 0xcf, 0x48, 0x15, 0x99, 0x70, 0x92, 0x49,
|
|
|
|
0xf6, 0xe8, 0xe5, 0xe2, 0x6c, 0x73, 0x8c, 0x48, 0x25, 0xed, 0x01, 0x72, 0xf7, 0x6c, 0x17, 0x28
|
|
|
|
};
|
|
|
|
|
|
|
|
#elif defined(Q_OS_OSX)
|
|
|
|
const QString AutoUpdater::platform = "osx";
|
|
|
|
const QString AutoUpdater::updaterBin = "installer -store -pkg "+Settings::getInstance().getSettingsDirPath()
|
|
|
|
+"/update/qtox.pkg -target /";
|
|
|
|
const QString AutoUpdater::updateServer = "https://dist-build.tox.im";
|
|
|
|
|
|
|
|
unsigned char AutoUpdater::key[crypto_sign_PUBLICKEYBYTES] =
|
|
|
|
{
|
|
|
|
0xa5, 0x80, 0xf3, 0xb7, 0xd0, 0x10, 0xc0, 0xf9, 0xd6, 0xcf, 0x48, 0x15, 0x99, 0x70, 0x92, 0x49,
|
|
|
|
0xf6, 0xe8, 0xe5, 0xe2, 0x6c, 0x73, 0x8c, 0x48, 0x25, 0xed, 0x01, 0x72, 0xf7, 0x6c, 0x17, 0x28
|
|
|
|
};
|
|
|
|
|
2014-11-09 22:51:00 +08:00
|
|
|
#else
|
2014-11-10 06:24:23 +08:00
|
|
|
const QString AutoUpdater::platform;
|
|
|
|
const QString AutoUpdater::updaterBin;
|
2014-11-12 05:37:51 +08:00
|
|
|
const QString AutoUpdater::updateServer;
|
|
|
|
unsigned char AutoUpdater::key[crypto_sign_PUBLICKEYBYTES];
|
2014-11-09 22:51:00 +08:00
|
|
|
#endif
|
|
|
|
const QString AutoUpdater::checkURI = AutoUpdater::updateServer+"/qtox/"+AutoUpdater::platform+"/version";
|
|
|
|
const QString AutoUpdater::flistURI = AutoUpdater::updateServer+"/qtox/"+AutoUpdater::platform+"/flist";
|
|
|
|
const QString AutoUpdater::filesURI = AutoUpdater::updateServer+"/qtox/"+AutoUpdater::platform+"/files/";
|
|
|
|
|
|
|
|
bool AutoUpdater::isUpdateAvailable()
|
|
|
|
{
|
2014-11-13 21:09:35 +08:00
|
|
|
VersionInfo newVersion = getUpdateVersion();
|
|
|
|
if (newVersion.timestamp <= TIMESTAMP
|
|
|
|
|| newVersion.versionString.isEmpty() || newVersion.versionString == GIT_VERSION)
|
2014-11-09 22:51:00 +08:00
|
|
|
return false;
|
|
|
|
else
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2014-11-13 21:09:35 +08:00
|
|
|
AutoUpdater::VersionInfo AutoUpdater::getUpdateVersion()
|
2014-11-09 22:51:00 +08:00
|
|
|
{
|
2014-11-13 21:09:35 +08:00
|
|
|
VersionInfo versionInfo;
|
|
|
|
versionInfo.timestamp = 0;
|
2014-11-09 22:51:00 +08:00
|
|
|
|
|
|
|
// Updates only for supported platforms
|
|
|
|
if (platform.isEmpty())
|
2014-11-13 21:09:35 +08:00
|
|
|
return versionInfo;
|
2014-11-09 22:51:00 +08:00
|
|
|
|
|
|
|
QNetworkAccessManager *manager = new QNetworkAccessManager;
|
|
|
|
QNetworkReply* reply = manager->get(QNetworkRequest(QUrl(checkURI)));
|
|
|
|
while (!reply->isFinished())
|
|
|
|
qApp->processEvents();
|
|
|
|
|
|
|
|
if (reply->error() != QNetworkReply::NoError)
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater: getUpdateVersion: network error: "<<reply->errorString();
|
2014-11-10 00:55:38 +08:00
|
|
|
reply->deleteLater();
|
|
|
|
manager->deleteLater();
|
2014-11-13 21:09:35 +08:00
|
|
|
return versionInfo;
|
2014-11-09 22:51:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
QByteArray data = reply->readAll();
|
2014-11-10 00:55:38 +08:00
|
|
|
reply->deleteLater();
|
|
|
|
manager->deleteLater();
|
|
|
|
if (data.size() < (int)(1+crypto_sign_BYTES))
|
2014-11-13 21:09:35 +08:00
|
|
|
return versionInfo;
|
2014-11-09 22:51:00 +08:00
|
|
|
|
|
|
|
// Check updater protocol version
|
2014-11-13 21:09:35 +08:00
|
|
|
if ((int)data[0] != '2')
|
2014-11-09 22:51:00 +08:00
|
|
|
{
|
2014-11-10 00:55:38 +08:00
|
|
|
qWarning() << "AutoUpdater: getUpdateVersion: Bad version "<<(uint8_t)data[0];
|
2014-11-13 21:09:35 +08:00
|
|
|
return versionInfo;
|
2014-11-09 22:51:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Check the signature
|
|
|
|
QByteArray sigData = data.mid(1, crypto_sign_BYTES);
|
|
|
|
unsigned char* sig = (unsigned char*)sigData.data();
|
|
|
|
QByteArray msgData = data.mid(1+crypto_sign_BYTES);
|
|
|
|
unsigned char* msg = (unsigned char*)msgData.data();
|
|
|
|
|
|
|
|
if (crypto_sign_verify_detached(sig, msg, msgData.size(), key) != 0)
|
|
|
|
{
|
|
|
|
qCritical() << "AutoUpdater: getUpdateVersion: RECEIVED FORGED VERSION FILE FROM "<<updateServer;
|
2014-11-13 21:09:35 +08:00
|
|
|
return versionInfo;
|
2014-11-09 22:51:00 +08:00
|
|
|
}
|
|
|
|
|
2014-11-13 21:09:35 +08:00
|
|
|
int sepPos = msgData.indexOf('!');
|
|
|
|
versionInfo.timestamp = QString(msgData.left(sepPos)).toInt();
|
|
|
|
versionInfo.versionString = msgData.mid(sepPos+1);
|
2014-11-09 22:51:00 +08:00
|
|
|
|
2014-11-13 21:09:35 +08:00
|
|
|
qDebug() << "timestamp:"<<versionInfo.timestamp << ", str:"<<versionInfo.versionString;
|
|
|
|
|
|
|
|
return versionInfo;
|
2014-11-09 22:51:00 +08:00
|
|
|
}
|
2014-11-10 00:55:38 +08:00
|
|
|
|
2014-11-10 06:24:23 +08:00
|
|
|
QList<AutoUpdater::UpdateFileMeta> AutoUpdater::parseFlist(QByteArray flistData)
|
2014-11-10 00:55:38 +08:00
|
|
|
{
|
|
|
|
QList<UpdateFileMeta> flist;
|
|
|
|
|
|
|
|
if (flistData.isEmpty())
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater::parseflist: Empty data";
|
|
|
|
return flist;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check version
|
|
|
|
if (flistData[0] != '1')
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater: parseflist: Bad version "<<(uint8_t)flistData[0];
|
|
|
|
return flist;
|
|
|
|
}
|
|
|
|
flistData = flistData.mid(1);
|
|
|
|
|
|
|
|
// Check signature
|
|
|
|
if (flistData.size() < (int)(crypto_sign_BYTES))
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater::parseflist: Truncated data";
|
|
|
|
return flist;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
QByteArray msgData = flistData.mid(crypto_sign_BYTES);
|
|
|
|
unsigned char* msg = (unsigned char*)msgData.data();
|
|
|
|
if (crypto_sign_verify_detached((unsigned char*)flistData.data(), msg, msgData.size(), key) != 0)
|
|
|
|
{
|
|
|
|
qCritical() << "AutoUpdater: parseflist: FORGED FLIST FILE";
|
|
|
|
return flist;
|
|
|
|
}
|
|
|
|
flistData = flistData.mid(crypto_sign_BYTES);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse. We assume no errors handling needed since the signature is valid.
|
|
|
|
while (!flistData.isEmpty())
|
|
|
|
{
|
|
|
|
UpdateFileMeta newFile;
|
|
|
|
|
|
|
|
memcpy(newFile.sig, flistData.data(), crypto_sign_BYTES);
|
|
|
|
flistData = flistData.mid(crypto_sign_BYTES);
|
|
|
|
|
|
|
|
newFile.id = dataToString(flistData);
|
|
|
|
flistData = flistData.mid(newFile.id.size() + getVUint32Size(flistData));
|
|
|
|
|
|
|
|
newFile.installpath = dataToString(flistData);
|
|
|
|
flistData = flistData.mid(newFile.installpath.size() + getVUint32Size(flistData));
|
|
|
|
|
|
|
|
newFile.size = dataToUint64(flistData);
|
|
|
|
flistData = flistData.mid(8);
|
|
|
|
|
|
|
|
flist += newFile;
|
|
|
|
}
|
|
|
|
|
|
|
|
return flist;
|
|
|
|
}
|
|
|
|
|
2014-11-10 06:24:23 +08:00
|
|
|
QByteArray AutoUpdater::getUpdateFlist()
|
2014-11-10 00:55:38 +08:00
|
|
|
{
|
2014-11-10 06:24:23 +08:00
|
|
|
QByteArray flist;
|
2014-11-10 00:55:38 +08:00
|
|
|
|
|
|
|
QNetworkAccessManager *manager = new QNetworkAccessManager;
|
|
|
|
QNetworkReply* reply = manager->get(QNetworkRequest(QUrl(flistURI)));
|
|
|
|
while (!reply->isFinished())
|
|
|
|
qApp->processEvents();
|
|
|
|
|
|
|
|
if (reply->error() != QNetworkReply::NoError)
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater: getUpdateFlist: network error: "<<reply->errorString();
|
|
|
|
reply->deleteLater();
|
|
|
|
manager->deleteLater();
|
|
|
|
return flist;
|
|
|
|
}
|
|
|
|
|
2014-11-10 06:24:23 +08:00
|
|
|
flist = reply->readAll();
|
2014-11-10 00:55:38 +08:00
|
|
|
reply->deleteLater();
|
|
|
|
manager->deleteLater();
|
|
|
|
|
|
|
|
return flist;
|
|
|
|
}
|
2014-11-10 06:24:23 +08:00
|
|
|
|
|
|
|
QByteArray AutoUpdater::getLocalFlist()
|
|
|
|
{
|
|
|
|
QByteArray flist;
|
|
|
|
|
|
|
|
QFile flistFile("flist");
|
|
|
|
if (!flistFile.open(QIODevice::ReadOnly))
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater::getLocalFlist: Can't open local flist";
|
|
|
|
return flist;
|
|
|
|
}
|
|
|
|
|
|
|
|
flist = flistFile.readAll();
|
|
|
|
flistFile.close();
|
|
|
|
|
|
|
|
return flist;
|
|
|
|
}
|
|
|
|
|
|
|
|
QList<AutoUpdater::UpdateFileMeta> AutoUpdater::genUpdateDiff(QList<UpdateFileMeta> updateFlist)
|
|
|
|
{
|
|
|
|
QList<UpdateFileMeta> diff;
|
|
|
|
QList<UpdateFileMeta> localFlist = parseFlist(getLocalFlist());
|
|
|
|
|
|
|
|
for (UpdateFileMeta file : updateFlist)
|
|
|
|
if (!localFlist.contains(file))
|
|
|
|
diff += file;
|
|
|
|
|
|
|
|
return diff;
|
|
|
|
}
|
|
|
|
|
|
|
|
AutoUpdater::UpdateFile AutoUpdater::getUpdateFile(UpdateFileMeta fileMeta)
|
|
|
|
{
|
|
|
|
UpdateFile file;
|
|
|
|
file.metadata = fileMeta;
|
|
|
|
|
|
|
|
QNetworkAccessManager *manager = new QNetworkAccessManager;
|
|
|
|
QNetworkReply* reply = manager->get(QNetworkRequest(QUrl(filesURI+fileMeta.id)));
|
|
|
|
while (!reply->isFinished())
|
|
|
|
qApp->processEvents();
|
|
|
|
|
|
|
|
if (reply->error() != QNetworkReply::NoError)
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater: getUpdateFile: network error: "<<reply->errorString();
|
|
|
|
reply->deleteLater();
|
|
|
|
manager->deleteLater();
|
|
|
|
return file;
|
|
|
|
}
|
|
|
|
|
|
|
|
file.data = reply->readAll();
|
|
|
|
reply->deleteLater();
|
|
|
|
manager->deleteLater();
|
|
|
|
|
|
|
|
return file;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool AutoUpdater::downloadUpdate()
|
|
|
|
{
|
|
|
|
// Updates only for supported platforms
|
|
|
|
if (platform.isEmpty())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Get a list of files to update
|
|
|
|
QByteArray newFlistData = getUpdateFlist();
|
|
|
|
QList<UpdateFileMeta> newFlist = parseFlist(newFlistData);
|
|
|
|
QList<UpdateFileMeta> diff = genUpdateDiff(newFlist);
|
|
|
|
|
|
|
|
qDebug() << "AutoUpdater: Need to update "<<diff.size()<<" files";
|
|
|
|
|
|
|
|
// Create an empty directory to download updates into
|
|
|
|
QString updateDirStr = Settings::getInstance().getSettingsDirPath() + "/update/";
|
|
|
|
QDir updateDir(updateDirStr);
|
|
|
|
if (updateDir.exists())
|
|
|
|
updateDir.removeRecursively();
|
|
|
|
QDir().mkdir(updateDirStr);
|
|
|
|
updateDir = QDir(updateDirStr);
|
|
|
|
if (!updateDir.exists())
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater::downloadUpdate: Can't create update directory, aborting...";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write the new flist for the updater
|
|
|
|
QFile newFlistFile(updateDirStr+"flist");
|
|
|
|
if (!newFlistFile.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater::downloadUpdate: Can't save new flist file, aborting...";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
newFlistFile.write(newFlistData);
|
|
|
|
newFlistFile.close();
|
|
|
|
|
|
|
|
// Download and write each new file
|
|
|
|
for (UpdateFileMeta fileMeta : diff)
|
|
|
|
{
|
|
|
|
qDebug() << "AutoUpdater: Downloading '"+fileMeta.installpath+"' ...";
|
|
|
|
|
|
|
|
// Create subdirs if necessary
|
|
|
|
QString fileDirStr{QFileInfo(updateDirStr+fileMeta.installpath).absolutePath()};
|
|
|
|
if (!QDir(fileDirStr).exists())
|
|
|
|
QDir().mkpath(fileDirStr);
|
|
|
|
|
|
|
|
// Download
|
|
|
|
UpdateFile file = getUpdateFile(fileMeta);
|
|
|
|
if (file.data.isNull())
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater::downloadUpdate: Error downloading a file, aborting...";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check signature
|
|
|
|
if (crypto_sign_verify_detached(file.metadata.sig, (unsigned char*)file.data.data(),
|
|
|
|
file.data.size(), key) != 0)
|
|
|
|
{
|
|
|
|
qCritical() << "AutoUpdater: downloadUpdate: RECEIVED FORGED FILE, aborting...";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save
|
|
|
|
QFile fileFile(updateDirStr+fileMeta.installpath);
|
|
|
|
if (!fileFile.open(QIODevice::WriteOnly | QIODevice::Truncate))
|
|
|
|
{
|
|
|
|
qWarning() << "AutoUpdater::downloadUpdate: Can't save new update file, aborting...";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
fileFile.write(file.data);
|
|
|
|
fileFile.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool AutoUpdater::isLocalUpdateReady()
|
|
|
|
{
|
|
|
|
// Updates only for supported platforms
|
|
|
|
if (platform.isEmpty())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Check that there's an update dir in the first place, valid or not
|
|
|
|
QString updateDirStr = Settings::getInstance().getSettingsDirPath() + "/update/";
|
|
|
|
QDir updateDir(updateDirStr);
|
|
|
|
if (!updateDir.exists())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
// Check that we have a flist and that every file on the diff exists
|
|
|
|
QFile updateFlistFile(updateDirStr+"flist");
|
|
|
|
if (!updateFlistFile.open(QIODevice::ReadOnly))
|
|
|
|
return false;
|
|
|
|
QByteArray updateFlistData = updateFlistFile.readAll();
|
|
|
|
updateFlistFile.close();
|
|
|
|
|
|
|
|
QList<UpdateFileMeta> updateFlist = parseFlist(updateFlistData);
|
|
|
|
QList<UpdateFileMeta> diff = genUpdateDiff(updateFlist);
|
|
|
|
|
|
|
|
for (UpdateFileMeta fileMeta : diff)
|
|
|
|
if (!QFile::exists(updateDirStr+fileMeta.installpath))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void AutoUpdater::installLocalUpdate()
|
|
|
|
{
|
|
|
|
qDebug() << "AutoUpdater: About to start the qTox updater to install a local update";
|
|
|
|
|
|
|
|
// Delete the update if we fail so we don't fail again.
|
|
|
|
|
|
|
|
// Updates only for supported platforms.
|
|
|
|
if (platform.isEmpty())
|
|
|
|
{
|
|
|
|
qCritical() << "AutoUpdater: Failed to start the qTox updater, removing the update and exiting";
|
|
|
|
QString updateDirStr = Settings::getInstance().getSettingsDirPath() + "/update/";
|
|
|
|
QDir(updateDirStr).removeRecursively();
|
|
|
|
exit(-1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Workaround QTBUG-7645
|
|
|
|
// QProcess fails silently when elevation is required instead of showing a UAC prompt on Win7/Vista
|
|
|
|
#ifdef Q_OS_WIN
|
|
|
|
int result = (int)::ShellExecuteA(0, "open", updaterBin.toUtf8().constData(), 0, 0, SW_SHOWNORMAL);
|
|
|
|
if (SE_ERR_ACCESSDENIED == result)
|
|
|
|
{
|
|
|
|
// Requesting elevation
|
|
|
|
result = (int)::ShellExecuteA(0, "runas", updaterBin.toUtf8().constData(), 0, 0, SW_SHOWNORMAL);
|
|
|
|
}
|
|
|
|
if (result <= 32)
|
|
|
|
{
|
|
|
|
goto fail;
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
if (!QProcess::startDetached(updaterBin))
|
|
|
|
goto fail;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
exit(0);
|
|
|
|
|
|
|
|
// Centralized error handling
|
|
|
|
fail:
|
|
|
|
qCritical() << "AutoUpdater: Failed to start the qTox updater, removing the update and exiting";
|
|
|
|
QString updateDirStr = Settings::getInstance().getSettingsDirPath() + "/update/";
|
|
|
|
QDir(updateDirStr).removeRecursively();
|
|
|
|
exit(-1);
|
|
|
|
}
|
2014-11-10 07:31:29 +08:00
|
|
|
|
|
|
|
void AutoUpdater::checkUpdatesAsyncInteractive()
|
|
|
|
{
|
|
|
|
QtConcurrent::run(&AutoUpdater::checkUpdatesAsyncInteractiveWorker);
|
|
|
|
}
|
|
|
|
|
|
|
|
void AutoUpdater::checkUpdatesAsyncInteractiveWorker()
|
|
|
|
{
|
|
|
|
if (!isUpdateAvailable())
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (Widget::getInstance()->askMsgboxQuestion(QObject::tr("Update", "The title of a message box"),
|
|
|
|
QObject::tr("An update is available, do you want to download it now ?\nIt will be installed when qTox restarts.")))
|
|
|
|
{
|
|
|
|
downloadUpdate();
|
|
|
|
}
|
|
|
|
}
|