summaryrefslogtreecommitdiff
path: root/src/nxmaccessmanager.cpp
diff options
context:
space:
mode:
authorisanae <14251494+isanae@users.noreply.github.com>2019-10-07 02:28:34 -0400
committerGitHub <noreply@github.com>2019-10-07 02:28:34 -0400
commit97d58a17bdf7b8b29b413c54490c863b875b8a59 (patch)
treee74d5561f536a56d930a14496334c4868496d240 /src/nxmaccessmanager.cpp
parent73556ad0845ee6dea2ec9f1fa50903de0448fa91 (diff)
parent925dbcba875477e1111453878f9b69e069bc8fd2 (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.cpp608
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();
+ }
+}