diff options
| author | isanae <14251494+isanae@users.noreply.github.com> | 2019-10-07 02:28:34 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-10-07 02:28:34 -0400 |
| commit | 97d58a17bdf7b8b29b413c54490c863b875b8a59 (patch) | |
| tree | e74d5561f536a56d930a14496334c4868496d240 /src/nxmaccessmanager.cpp | |
| parent | 73556ad0845ee6dea2ec9f1fa50903de0448fa91 (diff) | |
| parent | 925dbcba875477e1111453878f9b69e069bc8fd2 (diff) | |
Merge pull request #853 from isanae/validation-progress-dialog-fixes
Validation progress dialog fixes
Diffstat (limited to 'src/nxmaccessmanager.cpp')
| -rw-r--r-- | src/nxmaccessmanager.cpp | 608 |
1 files changed, 427 insertions, 181 deletions
diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 3cc1b7d9..5d8b8b9e 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -18,6 +18,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. */ #include "nxmaccessmanager.h" +#include "ui_validationprogressdialog.h" #include "iplugingame.h" #include "nexusinterface.h" #include "nxmurl.h" @@ -43,29 +44,18 @@ using namespace MOBase; using namespace std::chrono_literals; const QString NexusBaseUrl("https://api.nexusmods.com/v1"); -const std::chrono::seconds NXMAccessManager::ValidationTimeout = 10s; const QString NexusSSO("wss://sso.nexusmods.com"); const QString NexusSSOPage("https://www.nexusmods.com/sso?id=%1&application=modorganizer2"); -ValidationProgressDialog::ValidationProgressDialog(std::chrono::seconds t) : - m_timeout(t), m_bar(nullptr), m_buttons(nullptr), m_timer(nullptr), - m_first(true) +ValidationProgressDialog::ValidationProgressDialog(NexusKeyValidator& v) + : m_validator(v), m_updateTimer(nullptr), m_first(true) { - m_bar = new QProgressBar; - m_bar->setTextVisible(false); + ui.reset(new Ui::ValidationProgressDialog); + ui->setupUi(this); - auto* label = new QLabel(tr("Validating Nexus Connection")); - label->setAlignment(Qt::AlignHCenter); - - auto* vbox = new QVBoxLayout(this); - vbox->addWidget(label); - vbox->addWidget(m_bar); - - m_buttons = new QDialogButtonBox; - m_buttons->addButton(tr("Hide"), QDialogButtonBox::RejectRole); - connect(m_buttons, &QDialogButtonBox::clicked, [&](auto* b){ onButton(b); }); - vbox->addWidget(m_buttons); + connect(ui->hide, &QPushButton::clicked, [&]{ onHide(); }); + connect(ui->cancel, &QPushButton::clicked, [&]{ onCancel(); }); } void ValidationProgressDialog::setParentWidget(QWidget* w) @@ -75,30 +65,31 @@ void ValidationProgressDialog::setParentWidget(QWidget* w) hide(); setParent(w, windowFlags() | Qt::Dialog); setModal(false); - setVisible(wasVisible); + + if (w && wasVisible) { + setVisible(true); + raise(); + } } void ValidationProgressDialog::start() { - if (!m_timer) { - m_timer = new QTimer(this); - connect(m_timer, &QTimer::timeout, [&]{ onTimer(); }); - m_timer->setInterval(100ms); + if (!m_updateTimer) { + m_updateTimer = new QTimer(this); + connect(m_updateTimer, &QTimer::timeout, [&]{ onTimer(); }); + m_updateTimer->setInterval(100ms); } - m_bar->setRange(0, m_timeout.count()); - m_bar->setValue(0); - - m_elapsed.start(); - m_timer->start(); + updateProgress(); + m_updateTimer->start(); show(); } void ValidationProgressDialog::stop() { - if (m_timer) { - m_timer->stop(); + if (m_updateTimer) { + m_updateTimer->stop(); } hide(); @@ -118,25 +109,47 @@ void ValidationProgressDialog::closeEvent(QCloseEvent* e) e->ignore(); } -void ValidationProgressDialog::onButton(QAbstractButton* b) +void ValidationProgressDialog::onHide() { - if (m_buttons->buttonRole(b) == QDialogButtonBox::RejectRole) { - hide(); - } else { - qCritical() << "validation dialog: unknown button pressed"; - } + hide(); +} + +void ValidationProgressDialog::onCancel() +{ + m_validator.cancel(); } void ValidationProgressDialog::onTimer() { - m_bar->setValue(m_elapsed.elapsed() / 1000); + updateProgress(); +} + +void ValidationProgressDialog::updateProgress() +{ + const auto* current = m_validator.currentAttempt(); + + if (current) { + ui->progress->setRange(0, current->timeout().count()); + ui->progress->setValue(current->elapsed().elapsed() / 1000); + } else { + // indeterminate + ui->progress->setRange(0, 0); + } + + if (const auto* a=m_validator.lastAttempt()) { + ui->label->setText(a->message() + ". " + tr("Trying again...")); + } else if (current) { + ui->label->setText(tr("Connecting to Nexus...")); + } else { + ui->label->setText("?"); + } } NexusSSOLogin::NexusSSOLogin() : m_keyReceived(false), m_active(false) { - m_timeout.setInterval(NXMAccessManager::ValidationTimeout); + m_timeout.setInterval(10s); m_timeout.setSingleShot(true); QObject::connect( @@ -173,20 +186,22 @@ QString NexusSSOLogin::stateToString(States s, const QString& e) return QObject::tr("Waiting for Nexus..."); case WaitingForBrowser: - return QObject::tr( - "Opened Nexus in browser.\n" - "Switch to your browser and accept the request."); + return + QObject::tr("Opened Nexus in browser.") + "\n" + + QObject::tr("Switch to your browser and accept the request."); case Finished: return QObject::tr("Finished."); case Timeout: - return QObject::tr( - "No answer from Nexus.\n" - "A firewall might be blocking Mod Organizer."); + return + QObject::tr("No answer from Nexus.") + "\n" + + QObject::tr("A firewall might be blocking Mod Organizer."); case ClosedByRemote: - return QObject::tr("Nexus closed the connection."); + return + QObject::tr("Nexus closed the connection.") + "\n" + + QObject::tr("A firewall might be blocking Mod Organizer."); case Cancelled: return QObject::tr("Cancelled."); @@ -306,10 +321,11 @@ void NexusSSOLogin::onMessage(const QString& s) void NexusSSOLogin::onDisconnected() { if (m_active) { - m_active = false; - if (!m_keyReceived) { + close(); setState(ClosedByRemote); + } else { + m_active = false; } } } @@ -338,83 +354,48 @@ void NexusSSOLogin::onTimeout() } -NexusKeyValidator::NexusKeyValidator(NXMAccessManager& am) - : m_manager(am), m_reply(nullptr), m_active(false) +ValidationAttempt::ValidationAttempt(std::chrono::seconds timeout) + : m_reply(nullptr), m_result(None) { - m_timeout.setInterval(NXMAccessManager::ValidationTimeout); m_timeout.setSingleShot(true); + m_timeout.setInterval(timeout); QObject::connect(&m_timeout, &QTimer::timeout, [&]{ onTimeout(); }); } -NexusKeyValidator::~NexusKeyValidator() -{ - abort(); -} - -QString NexusKeyValidator::stateToString(States s, const QString& e) +void ValidationAttempt::start(NXMAccessManager& m, const QString& key) { - switch (s) - { - case NexusKeyValidator::Connecting: - return QObject::tr("Connecting to Nexus..."); - - case NexusKeyValidator::Finished: - return QObject::tr("Finished."); - - case NexusKeyValidator::InvalidJson: - return QObject::tr("Invalid JSON"); - - case NexusKeyValidator::BadResponse: - return QObject::tr("Bad response"); - - case NexusKeyValidator::Timeout: - return QObject::tr("There was a timeout during the request"); + if (!sendRequest(m, key)) { + return; + } - case NexusKeyValidator::Cancelled: - return QObject::tr("Cancelled"); + m_elapsed.start(); + m_timeout.start(); - case NexusKeyValidator::Error: // fall-through - default: - { - if (e.isEmpty()) { - return QString("%1").arg(s); - } else { - return e; - } - } - } + log::debug( + "validator: attempt started with timeout of {} seconds", timeout().count()); } -void NexusKeyValidator::start(const QString& key) +bool ValidationAttempt::sendRequest( + NXMAccessManager& m, const QString& key) { - if (m_reply) { - abort(); - return; - } - - m_active = true; - setState(Connecting); - const QString requestUrl(NexusBaseUrl + "/users/validate"); QNetworkRequest request(requestUrl); request.setRawHeader("APIKEY", key.toUtf8()); - request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_manager.userAgent().toUtf8()); + request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m.userAgent().toUtf8()); request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); request.setRawHeader("Protocol-Version", "1.0.0"); request.setRawHeader("Application-Name", "MO2"); - request.setRawHeader("Application-Version", m_manager.MOVersion().toUtf8()); + request.setRawHeader("Application-Version", m.MOVersion().toUtf8()); + + m_reply = m.get(request); - m_reply = m_manager.get(request); if (!m_reply) { - close(); - setState(Error, QObject::tr("Failed to request %1").arg(requestUrl)); - return; + setFailure(SoftError, QObject::tr("Failed to request %1").arg(requestUrl)); + return false; } - m_timeout.start(NXMAccessManager::ValidationTimeout); - QObject::connect( m_reply, &QNetworkReply::finished, [&]{ onFinished(); }); @@ -422,83 +403,114 @@ void NexusKeyValidator::start(const QString& key) QObject::connect( m_reply, &QNetworkReply::sslErrors, [&](auto&& errors){ onSslErrors(errors); }); + + return true; } -void NexusKeyValidator::cancel() +void ValidationAttempt::cancel() { - if (m_active) { - abort(); - setState(Cancelled); + if (!m_reply || m_result != None) { + // not running + return; } + + setFailure(Cancelled, QObject::tr("Cancelled")); + + if (m_reply) { + m_reply->abort(); + } + + cleanup(); } -bool NexusKeyValidator::isActive() const +bool ValidationAttempt::done() const { - return m_active; + return (m_result != None); } -void NexusKeyValidator::close() +ValidationAttempt::Result ValidationAttempt::result() const { - m_active = false; - m_timeout.stop(); - - if (m_reply) { - m_reply->disconnect(); - m_reply->deleteLater(); - m_reply = nullptr; - } + return m_result; } -void NexusKeyValidator::abort() +const QString& ValidationAttempt::message() const { - m_active = false; - m_timeout.stop(); + return m_message; +} - if (m_reply) { - m_reply->disconnect(); - m_reply->abort(); - m_reply->deleteLater(); - m_reply = nullptr; - } +std::chrono::seconds ValidationAttempt::timeout() const +{ + return std::chrono::duration_cast<std::chrono::seconds>( + m_timeout.intervalAsDuration()); } -void NexusKeyValidator::setState(States s, const QString& error) +QElapsedTimer ValidationAttempt::elapsed() const { - if (stateChanged) { - stateChanged(s, error); - } + return m_elapsed; } -void NexusKeyValidator::onFinished() +void ValidationAttempt::onFinished() { + if (m_result == Cancelled) { + return; + } + + log::debug("validator attempt: request has finished"); + if (!m_reply) { // shouldn't happen + log::error("validator attempt: reply is null"); + setFailure(HardError, QObject::tr("Internal error")); return; } - m_timeout.stop(); + const auto code = m_reply->attribute( + QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (code == 0) { + // request wasn't even sent + log::error("validator attempt: code is 0"); + setFailure(SoftError, m_reply->errorString()); + return; + } - const auto code = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const auto doc = QJsonDocument::fromJson(m_reply->readAll()); const auto headers = m_reply->rawHeaderPairs(); - const auto error = m_reply->errorString(); - - close(); + const auto httpError = m_reply->errorString(); const QJsonObject data = doc.object(); if (code != 200) { - handleError(code, data.value("message").toString(), error); + // http request failed + + QString s = m_reply->errorString(); + + const auto nexusMessage = data.value("message").toString(); + if (!nexusMessage.isEmpty()) { + if (!s.isEmpty()) { + s += ", "; + } + + s += nexusMessage; + } + + if (s.isEmpty()) { + s = QObject::tr("HTTP code %1").arg(code); + } else { + s += QString(" (%1)").arg(code); + } + + setFailure(SoftError, s); return; } if (doc.isNull()) { - setState(InvalidJson); + setFailure(HardError, QObject::tr("Invalid JSON")); return; } if (!data.contains("user_id")) { - setState(BadResponse); + setFailure(HardError, QObject::tr("Bad response")); return; } @@ -507,6 +519,11 @@ void NexusKeyValidator::onFinished() const QString name = data.value("name").toString(); const bool premium = data.value("is_premium").toBool(); + if (key.isEmpty()) { + setFailure(HardError, QObject::tr("API key is empty")); + return; + } + const auto user = APIUserAccount() .apiKey(key) .id(QString("%1").arg(id)) @@ -514,62 +531,241 @@ void NexusKeyValidator::onFinished() .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular) .limits(NexusInterface::parseLimits(headers)); - if (finished) { - setState(Finished); - finished(user); + setSuccess(user); +} + +void ValidationAttempt::onSslErrors(const QList<QSslError>& errors) +{ + log::error("validator attempt: ssl errors"); + + for (auto& e : errors) { + log::error(" . {}", e.errorString()); } + + setFailure(HardError, QObject::tr("SSL error")); } -void NexusKeyValidator::onSslErrors(const QList<QSslError>& errors) +void ValidationAttempt::onTimeout() { - if (m_active) { - for (const auto& e : errors) { - setState(Error, e.errorString()); - } + setFailure(SoftError, QObject::tr("Timed out")); +} + +void ValidationAttempt::setFailure(Result r, const QString& error) +{ + if (r != Cancelled) { + // don't spam the log + log::error("validator attempt: {}", error); + } + + cleanup(); + + m_result = r; + m_message = error; + + if (failure) { + failure(); } } -void NexusKeyValidator::onTimeout() +void ValidationAttempt::setSuccess(const APIUserAccount& user) { - abort(); - setState(Timeout); + log::debug("validator attempt successful"); + cleanup(); + + m_result = Success; + m_message = ""; + + if (success) { + success(user); + } } -void NexusKeyValidator::handleError( - int code, const QString& nexusMessage, const QString& httpError) +void ValidationAttempt::cleanup() { - QString s = httpError; + m_timeout.stop(); + + if (m_reply) { + m_reply->disconnect(); + m_reply->deleteLater(); + m_reply = nullptr; + } +} + + +NexusKeyValidator::NexusKeyValidator(NXMAccessManager& am) + : m_manager(am) +{ +} + +NexusKeyValidator::~NexusKeyValidator() +{ + cancel(); +} + +void NexusKeyValidator::start(const QString& key, Behaviour b) +{ + if (isActive()) { + log::debug("validator: trying to start while ongoing; ignoring"); + return; + } + + m_key = key; - if (!nexusMessage.isEmpty()) { - if (!s.isEmpty()) { - s += ", "; + switch (b) + { + case OneShot: + { + createAttempts({10s}); + break; } - s += nexusMessage; + case Retry: + { + createAttempts({5s, 5s, 10s}); + break; + } } - if (code != 0) { - if (s.isEmpty()) { - s = QString("HTTP code %1").arg(code); + nextTry(); +} + +void NexusKeyValidator::createAttempts( + const std::vector<std::chrono::seconds>& timeouts) +{ + m_attempts.clear(); + + for (auto&& t : timeouts) { + m_attempts.push_back(std::make_unique<ValidationAttempt>(t)); + } +} + +void NexusKeyValidator::cancel() +{ + log::debug("validator: cancelled"); + + for (auto&& a : m_attempts) { + a->cancel(); + } +} + +bool NexusKeyValidator::isActive() const +{ + for (auto&& a : m_attempts) { + if (!a->done()) { + return true; + } + } + + return false; +} + +const ValidationAttempt* NexusKeyValidator::lastAttempt() const +{ + const ValidationAttempt* last = nullptr; + + for (auto&& a : m_attempts) { + if (a->done()) { + last = a.get(); } else { - s += QString(" (%1)").arg(code); + break; } } - setState(Error, s); + return last; } +const ValidationAttempt* NexusKeyValidator::currentAttempt() const +{ + for (auto&& a : m_attempts) { + if (!a->done()) { + return a.get(); + } + } + + return nullptr; +} + +bool NexusKeyValidator::nextTry() +{ + for (auto&& a : m_attempts) { + if (!a->done()) { + a->success = [&](auto&& user){ onAttemptSuccess(*a, user); }; + a->failure = [&]{ onAttemptFailure(*a); }; + + a->start(m_manager, m_key); + return true; + } + } + + // no more + return false; +} + +void NexusKeyValidator::onAttemptSuccess( + const ValidationAttempt& a, const APIUserAccount& u) +{ + if (attemptFinished) { + attemptFinished(a); + } + + setFinished(ValidationAttempt::Success, "", u); +} + +void NexusKeyValidator::onAttemptFailure(const ValidationAttempt& a) +{ + if (attemptFinished) { + attemptFinished(a); + } + + switch (a.result()) + { + case ValidationAttempt::SoftError: + { + if (!nextTry()) { + setFinished(a.result(), a.message(), {}); + } + + break; + } + + case ValidationAttempt::HardError: + { + cancel(); + setFinished(a.result(), a.message(), {}); + break; + } + + case ValidationAttempt::Cancelled: + { + setFinished(ValidationAttempt::Cancelled, QObject::tr("Cancelled"), {}); + break; + } + } +} + +void NexusKeyValidator::setFinished( + ValidationAttempt::Result r, const QString& message, + std::optional<APIUserAccount> user) +{ + if (finished) { + finished(r, message, user); + } +} NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion) : QNetworkAccessManager(parent) - , m_ProgressDialog(new ValidationProgressDialog(ValidationTimeout)) , m_MOVersion(moVersion) , m_validator(*this) , m_validationState(NotChecked) { - m_validator.stateChanged = [&](auto&& s, auto&& e){ onValidatorState(s, e); }; - m_validator.finished = [&](auto&& user){ onValidatorFinished(user); }; + m_validator.finished = [&](auto&& r, auto&& m, auto&& u) { + onValidatorFinished(r, m, u); + }; + + m_validator.attemptFinished = [&](auto&& a) { + onValidatorAttemptFinished(a); + }; setCookieJar(new PersistentCookieJar(QDir::fromNativeSeparators( Settings::instance().paths().cache() + "/nexus_cookies.dat"))); @@ -582,7 +778,14 @@ NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion) void NXMAccessManager::setTopLevelWidget(QWidget* w) { - m_ProgressDialog->setParentWidget(w); + if (w) { + if (m_ProgressDialog) { + m_ProgressDialog->setParentWidget(w); + } + } else { + m_ProgressDialog.reset(); + m_validator.cancel(); + } } QNetworkReply *NXMAccessManager::createRequest( @@ -628,36 +831,63 @@ void NXMAccessManager::clearCookies() void NXMAccessManager::startValidationCheck(const QString& key) { m_validationState = NotChecked; - m_validator.start(key); - m_ProgressDialog->start(); + m_validator.start(key, NexusKeyValidator::Retry); + + if (m_ProgressDialog) { + // don't show the progress dialog on startup for the first attempt; the + // dialog will be shown in onValidatorAttemptFinished() if it failed + startProgress(); + } } -void NXMAccessManager::onValidatorState( - NexusKeyValidator::States s, const QString& e) +void NXMAccessManager::onValidatorFinished( + ValidationAttempt::Result r, const QString& message, + std::optional<APIUserAccount> user) { - if (s == NexusKeyValidator::Connecting || s == NexusKeyValidator::Finished) { - // no-op, success is handled in onValidatorFinished() - return; - } + stopProgress(); - m_ProgressDialog->stop(); - m_validationState = Invalid; - emit validateFailed(NexusKeyValidator::stateToString(s, e)); + if (user) { + m_validationState = Valid; + emit credentialsReceived(*user); + emit validateSuccessful(true); + } else { + if (r == ValidationAttempt::Cancelled) { + m_validationState = NotChecked; + } else { + m_validationState = Invalid; + emit validateFailed(message); + } + } } -void NXMAccessManager::onValidatorFinished(const APIUserAccount& user) +void NXMAccessManager::onValidatorAttemptFinished(const ValidationAttempt& a) { - m_ProgressDialog->stop(); + if (!m_ProgressDialog) { + switch (a.result()) + { + case ValidationAttempt::SoftError: + case ValidationAttempt::HardError: + { + startProgress(); + break; + } - m_validationState = Valid; - emit credentialsReceived(user); - emit validateSuccessful(true); + case ValidationAttempt::None: + case ValidationAttempt::Success: + case ValidationAttempt::Cancelled: + default: + { + // don't show the dialog + break; + } + } + } } bool NXMAccessManager::validated() const { if (m_validator.isActive()) { - m_ProgressDialog->show(); + const_cast<NXMAccessManager*>(this)->startProgress(); } return (m_validationState == Valid); @@ -723,3 +953,19 @@ void NXMAccessManager::clearApiKey() m_validator.cancel(); emit credentialsReceived(APIUserAccount()); } + +void NXMAccessManager::startProgress() +{ + if (!m_ProgressDialog) { + m_ProgressDialog.reset(new ValidationProgressDialog(m_validator)); + } + + m_ProgressDialog->start(); +} + +void NXMAccessManager::stopProgress() +{ + if (m_ProgressDialog) { + m_ProgressDialog->stop(); + } +} |
