summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJeremy Rimpo <jrim@rimpo.org>2019-07-22 01:02:21 -0500
committerGitHub <noreply@github.com>2019-07-22 01:02:21 -0500
commit1f1b838c2b3fd3148d2c2a1a8fe6fcf9fb07383f (patch)
tree33c20c0cc6e5f0029558c8e8383a0fdf68571689 /src
parent613aa037cf8dc728eac474ee6cf6b790293d5114 (diff)
parent2d78957c3d4c33de5813d7ee5b86dec8cb01478c (diff)
Merge pull request #796 from isanae/login-progress-dialog
Nexus login progress dialog and Nexus settings
Diffstat (limited to 'src')
-rw-r--r--src/apiuseraccount.cpp33
-rw-r--r--src/apiuseraccount.h21
-rw-r--r--src/downloadlistwidget.cpp67
-rw-r--r--src/main.cpp11
-rw-r--r--src/mainwindow.cpp6
-rw-r--r--src/mainwindow.h2
-rw-r--r--src/nexusinterface.cpp25
-rw-r--r--src/nexusinterface.h1
-rw-r--r--src/nxmaccessmanager.cpp721
-rw-r--r--src/nxmaccessmanager.h170
-rw-r--r--src/organizercore.cpp3
-rw-r--r--src/pch.h1
-rw-r--r--src/selfupdater.cpp2
-rw-r--r--src/settingsdialog.cpp246
-rw-r--r--src/settingsdialog.h34
-rw-r--r--src/settingsdialog.ui343
16 files changed, 1220 insertions, 466 deletions
diff --git a/src/apiuseraccount.cpp b/src/apiuseraccount.cpp
index b901e41a..35a868d5 100644
--- a/src/apiuseraccount.cpp
+++ b/src/apiuseraccount.cpp
@@ -1,10 +1,37 @@
#include "apiuseraccount.h"
+QString localizedUserAccountType(APIUserAccountTypes t)
+{
+ switch (t)
+ {
+ case APIUserAccountTypes::Regular:
+ return QObject::tr("Regular");
+
+ case APIUserAccountTypes::Premium:
+ return QObject::tr("Premium");
+
+ case APIUserAccountTypes::None: // fall-through
+ default:
+ return QObject::tr("None");
+ }
+}
+
+
APIUserAccount::APIUserAccount()
: m_type(APIUserAccountTypes::None)
{
}
+bool APIUserAccount::isValid() const
+{
+ return !m_key.isEmpty();
+}
+
+const QString& APIUserAccount::apiKey() const
+{
+ return m_key;
+}
+
const QString& APIUserAccount::id() const
{
return m_id;
@@ -25,6 +52,12 @@ const APILimits& APIUserAccount::limits() const
return m_limits;
}
+APIUserAccount& APIUserAccount::apiKey(const QString& key)
+{
+ m_key = key;
+ return *this;
+}
+
APIUserAccount& APIUserAccount::id(const QString& id)
{
m_id = id;
diff --git a/src/apiuseraccount.h b/src/apiuseraccount.h
index 8a238d71..ea4e8685 100644
--- a/src/apiuseraccount.h
+++ b/src/apiuseraccount.h
@@ -18,6 +18,8 @@ enum class APIUserAccountTypes
Premium
};
+QString localizedUserAccountType(APIUserAccountTypes t);
+
/**
* current limits imposed on the user account
@@ -60,6 +62,17 @@ public:
APIUserAccount();
+
+ /**
+ * whether the user is logged in
+ */
+ bool isValid() const;
+
+ /**
+ * api key
+ */
+ const QString& apiKey() const;
+
/**
* user id
*/
@@ -82,6 +95,11 @@ public:
/**
+ * sets the api key
+ */
+ APIUserAccount& apiKey(const QString& key);
+
+ /**
* sets the user id
*/
APIUserAccount& id(const QString& id);
@@ -120,10 +138,9 @@ public:
bool exhausted() const;
private:
- QString m_id, m_name;
+ QString m_key, m_id, m_name;
APIUserAccountTypes m_type;
APILimits m_limits;
- APIStats m_stats;
};
#endif // APIUSERACCOUNT_H
diff --git a/src/downloadlistwidget.cpp b/src/downloadlistwidget.cpp
index c75ecdd2..e2a6f321 100644
--- a/src/downloadlistwidget.cpp
+++ b/src/downloadlistwidget.cpp
@@ -201,40 +201,49 @@ void DownloadListWidget::onCustomContextMenu(const QPoint &point)
QModelIndex index = indexAt(point);
bool hidden = false;
- if (index.row() >= 0) {
- m_ContextRow = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index).row();
- DownloadManager::DownloadState state = m_Manager->getState(m_ContextRow);
- hidden = m_Manager->isHidden(m_ContextRow);
+ try
+ {
+ if (index.row() >= 0) {
+ m_ContextRow = qobject_cast<QSortFilterProxyModel*>(model())->mapToSource(index).row();
+ DownloadManager::DownloadState state = m_Manager->getState(m_ContextRow);
- if (state >= DownloadManager::STATE_READY) {
- menu.addAction(tr("Install"), this, SLOT(issueInstall()));
- if (m_Manager->isInfoIncomplete(m_ContextRow))
- menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfoMd5()));
- else
- menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus()));
- menu.addAction(tr("Open File"), this, SLOT(issueOpenFile()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
+ hidden = m_Manager->isHidden(m_ContextRow);
- menu.addSeparator();
+ if (state >= DownloadManager::STATE_READY) {
+ menu.addAction(tr("Install"), this, SLOT(issueInstall()));
+ if (m_Manager->isInfoIncomplete(m_ContextRow))
+ menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfoMd5()));
+ else
+ menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus()));
+ menu.addAction(tr("Open File"), this, SLOT(issueOpenFile()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
- if (hidden)
- menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView()));
- else
- menu.addAction(tr("Hide"), this, SLOT(issueRemoveFromView()));
- } else if (state == DownloadManager::STATE_DOWNLOADING) {
- menu.addAction(tr("Cancel"), this, SLOT(issueCancel()));
- menu.addAction(tr("Pause"), this, SLOT(issuePause()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)
- || (state == DownloadManager::STATE_PAUSING)) {
- menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
- menu.addAction(tr("Resume"), this, SLOT(issueResume()));
- menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
- }
+ menu.addSeparator();
- menu.addSeparator();
+ menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
+ if (hidden)
+ menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView()));
+ else
+ menu.addAction(tr("Hide"), this, SLOT(issueRemoveFromView()));
+ } else if (state == DownloadManager::STATE_DOWNLOADING) {
+ menu.addAction(tr("Cancel"), this, SLOT(issueCancel()));
+ menu.addAction(tr("Pause"), this, SLOT(issuePause()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
+ } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)
+ || (state == DownloadManager::STATE_PAUSING)) {
+ menu.addAction(tr("Delete"), this, SLOT(issueDelete()));
+ menu.addAction(tr("Resume"), this, SLOT(issueResume()));
+ menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder()));
+ }
+
+ menu.addSeparator();
+ }
+ } catch(std::exception&)
+ {
+ // this happens when the download index is not found, ignore it and don't
+ // display download-specific actions
}
+
menu.addAction(tr("Delete Installed Downloads..."), this, SLOT(issueDeleteCompleted()));
menu.addAction(tr("Delete Uninstalled Downloads..."), this, SLOT(issueDeleteUninstalled()));
menu.addAction(tr("Delete All Downloads..."), this, SLOT(issueDeleteAll()));
diff --git a/src/main.cpp b/src/main.cpp
index f2571685..4359c645 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -697,6 +697,9 @@ int runApplication(MOApplication &application, SingleInstance &instance,
// set up main window and its data structures
MainWindow mainWindow(settings, organizer, pluginContainer);
+ NexusInterface::instance(&pluginContainer)
+ ->getAccessManager()->setTopLevelWidget(&mainWindow);
+
QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application,
SLOT(setStyleFile(QString)));
QObject::connect(&instance, SIGNAL(messageSent(QString)), &organizer,
@@ -710,7 +713,13 @@ int runApplication(MOApplication &application, SingleInstance &instance,
mainWindow.activateWindow();
splash.finish(&mainWindow);
- return application.exec();
+
+ const auto ret = application.exec();
+
+ NexusInterface::instance(&pluginContainer)
+ ->getAccessManager()->setTopLevelWidget(nullptr);
+
+ return ret;
}
} catch (const std::exception &e) {
reportError(e.what());
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 451ec688..9586cb93 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -399,7 +399,6 @@ MainWindow::MainWindow(QSettings &initSettings
connect(NexusInterface::instance(&pluginContainer), SIGNAL(requestNXMDownload(QString)), &m_OrganizerCore, SLOT(downloadRequestedNXM(QString)));
connect(NexusInterface::instance(&pluginContainer), SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(QString,int,int,QVariant,QVariant,int)));
connect(NexusInterface::instance(&pluginContainer), SIGNAL(needLogin()), &m_OrganizerCore, SLOT(nexusApi()));
- connect(NexusInterface::instance(&pluginContainer)->getAccessManager(), SIGNAL(validateFailed(QString)), this, SLOT(validationFailed(QString)));
connect(
NexusInterface::instance(&pluginContainer)->getAccessManager(),
@@ -3095,11 +3094,6 @@ void MainWindow::untrack_clicked()
});
}
-void MainWindow::validationFailed(const QString &error)
-{
- qDebug("Nexus API validation failed: %s", qUtf8Printable(error));
-}
-
void MainWindow::windowTutorialFinished(const QString &windowName)
{
m_OrganizerCore.settings().directInterface().setValue(QString("CompletedWindowTutorials/") + windowName, true);
diff --git a/src/mainwindow.h b/src/mainwindow.h
index eee269cf..80508787 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -510,8 +510,6 @@ private slots:
// nexus related
void checkModsForUpdates();
- void validationFailed(const QString &message);
-
void linkClicked(const QString &url);
void updateAvailable();
diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp
index ee9acf2c..2bcd72f3 100644
--- a/src/nexusinterface.cpp
+++ b/src/nexusinterface.cpp
@@ -208,12 +208,27 @@ APILimits NexusInterface::defaultAPILimits()
APILimits NexusInterface::parseLimits(const QNetworkReply* reply)
{
+ return parseLimits(reply->rawHeaderPairs());
+}
+
+APILimits NexusInterface::parseLimits(
+ const QList<QNetworkReply::RawHeaderPair>& headers)
+{
APILimits limits;
- limits.maxDailyRequests = reply->rawHeader("x-rl-daily-limit").toInt();
- limits.remainingDailyRequests = reply->rawHeader("x-rl-daily-remaining").toInt();
- limits.maxHourlyRequests = reply->rawHeader("x-rl-hourly-limit").toInt();
- limits.remainingHourlyRequests = reply->rawHeader("x-rl-hourly-remaining").toInt();
+ for (const auto& pair : headers) {
+ const auto name = QString(pair.first).toLower();
+
+ if (name == "x-rl-daily-limit") {
+ limits.maxDailyRequests = pair.second.toInt();
+ } else if (name == "x-rl-daily-remaining") {
+ limits.remainingDailyRequests = pair.second.toInt();
+ } else if (name == "x-rl-hourly-limit") {
+ limits.maxHourlyRequests = pair.second.toInt();
+ } else if (name == "x-rl-hourly-remaining") {
+ limits.remainingHourlyRequests = pair.second.toInt();
+ }
+ }
return limits;
}
@@ -765,7 +780,7 @@ void NexusInterface::nextRequest()
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
- request.setRawHeader("APIKEY", m_AccessManager->apiKey().toUtf8());
+ request.setRawHeader("APIKEY", m_User.apiKey().toUtf8());
request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
request.setRawHeader("Protocol-Version", "1.0.0");
diff --git a/src/nexusinterface.h b/src/nexusinterface.h
index 6e768149..0b1763c4 100644
--- a/src/nexusinterface.h
+++ b/src/nexusinterface.h
@@ -151,6 +151,7 @@ public:
public:
static APILimits defaultAPILimits();
static APILimits parseLimits(const QNetworkReply* reply);
+ static APILimits parseLimits(const QList<QNetworkReply::RawHeaderPair>& headers);
~NexusInterface();
diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp
index ee6c03f1..c413e156 100644
--- a/src/nxmaccessmanager.cpp
+++ b/src/nxmaccessmanager.cpp
@@ -40,21 +40,530 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QJsonArray>
using namespace MOBase;
+using namespace std::chrono_literals;
-namespace {
- QString const nexusBaseUrl("https://api.nexusmods.com/v1");
+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_bar = new QProgressBar;
+ m_bar->setTextVisible(false);
+
+ 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);
+}
+
+void ValidationProgressDialog::setParentWidget(QWidget* w)
+{
+ const auto wasVisible = isVisible();
+
+ hide();
+ setParent(w, windowFlags() | Qt::Dialog);
+ setModal(false);
+ setVisible(wasVisible);
+}
+
+void ValidationProgressDialog::start()
+{
+ if (!m_timer) {
+ m_timer = new QTimer(this);
+ connect(m_timer, &QTimer::timeout, [&]{ onTimer(); });
+ m_timer->setInterval(100ms);
+ }
+
+ m_bar->setRange(0, m_timeout.count());
+ m_bar->setValue(0);
+
+ m_elapsed.start();
+ m_timer->start();
+
+ show();
+}
+
+void ValidationProgressDialog::stop()
+{
+ if (m_timer) {
+ m_timer->stop();
+ }
+
+ hide();
+}
+
+void ValidationProgressDialog::closeEvent(QCloseEvent* e)
+{
+ hide();
+ e->ignore();
+}
+
+void ValidationProgressDialog::onButton(QAbstractButton* b)
+{
+ if (m_buttons->buttonRole(b) == QDialogButtonBox::RejectRole) {
+ hide();
+ } else {
+ qCritical() << "validation dialog: unknown button pressed";
+ }
+}
+
+void ValidationProgressDialog::onTimer()
+{
+ m_bar->setValue(m_elapsed.elapsed() / 1000);
+}
+
+
+NexusSSOLogin::NexusSSOLogin()
+ : m_keyReceived(false), m_active(false)
+{
+ m_timeout.setInterval(NXMAccessManager::ValidationTimeout);
+ m_timeout.setSingleShot(true);
+
+ QObject::connect(
+ &m_socket, &QWebSocket::connected,
+ [&]{ onConnected(); });
+
+ QObject::connect(
+ &m_socket, qOverload<QAbstractSocket::SocketError>(&QWebSocket::error),
+ [&](auto&& e){ onError(e); });
+
+ QObject::connect(
+ &m_socket, &QWebSocket::sslErrors,
+ [&](auto&& errors){ onSslErrors(errors); });
+
+ QObject::connect(
+ &m_socket, &QWebSocket::textMessageReceived,
+ [&](auto&& s){ onMessage(s); });
+
+ QObject::connect(
+ &m_socket, &QWebSocket::disconnected,
+ [&]{ onDisconnected(); });
+
+ QObject::connect(&m_timeout, &QTimer::timeout, [&]{ onTimeout(); });
+}
+
+QString NexusSSOLogin::stateToString(States s, const QString& e)
+{
+ switch (s)
+ {
+ case ConnectingToSSO:
+ return QObject::tr("Connecting to Nexus...");
+
+ case WaitingForToken:
+ return QObject::tr("Waiting for Nexus...");
+
+ case WaitingForBrowser:
+ return QObject::tr(
+ "Opened Nexus in browser.\n"
+ "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.");
+
+ case ClosedByRemote:
+ return QObject::tr("Nexus closed the connection.");
+
+ case Cancelled:
+ return QObject::tr("Cancelled.");
+
+ case Error: // fall-through
+ default:
+ {
+ if (e.isEmpty()) {
+ return QString("%1").arg(s);
+ } else {
+ return e;
+ }
+ }
+ }
+}
+
+void NexusSSOLogin::start()
+{
+ m_active = true;
+ setState(ConnectingToSSO);
+ m_timeout.start();
+ m_socket.open(NexusSSO);
+}
+
+void NexusSSOLogin::cancel()
+{
+ if (m_active) {
+ abort();
+ setState(Cancelled);
+ }
+}
+
+void NexusSSOLogin::close()
+{
+ if (m_active) {
+ m_active = false;
+ m_timeout.stop();
+ m_socket.close();
+ }
+}
+
+void NexusSSOLogin::abort()
+{
+ m_active = false;
+ m_timeout.stop();
+ m_socket.abort();
+}
+
+bool NexusSSOLogin::isActive() const
+{
+ return m_active;
+}
+
+void NexusSSOLogin::setState(States s, const QString& error)
+{
+ if (stateChanged) {
+ stateChanged(s, error);
+ }
+}
+
+void NexusSSOLogin::onConnected()
+{
+ setState(WaitingForToken);
+
+ m_keyReceived = false;
+
+ boost::uuids::random_generator generator;
+ boost::uuids::uuid sessionId = generator();
+ m_guid = boost::uuids::to_string(sessionId).c_str();
+
+ QJsonObject data;
+ data.insert(QString("id"), QJsonValue(m_guid));
+ data.insert(QString("protocol"), 2);
+
+ const QString message = QJsonDocument(data).toJson();
+ m_socket.sendTextMessage(message);
+}
+
+void NexusSSOLogin::onMessage(const QString& s)
+{
+ const QJsonDocument doc = QJsonDocument::fromJson(s.toUtf8());
+ const QVariantMap root = doc.object().toVariantMap();
+
+ if (!root["success"].toBool()) {
+ close();
+
+ setState(Error, QString("There was a problem with SSO initialization: %1")
+ .arg(root["error"].toString()));
+
+ return;
+ }
+
+ const QVariantMap data = root["data"].toMap();
+
+ if (data.contains("connection_token")) {
+ // first answer
+
+ // open browser
+ const auto url = NexusSSOPage.arg(m_guid);
+ shell::OpenLink(url);
+
+ m_timeout.stop();
+ setState(WaitingForBrowser);
+ } else {
+ // second answer
+ const auto key = data["api_key"].toString();
+ close();
+
+ if (keyChanged) {
+ keyChanged(key);
+ }
+
+ setState(Finished);
+ }
+}
+
+void NexusSSOLogin::onDisconnected()
+{
+ if (m_active) {
+ m_active = false;
+
+ if (!m_keyReceived) {
+ setState(ClosedByRemote);
+ }
+ }
+}
+
+void NexusSSOLogin::onError(QAbstractSocket::SocketError e)
+{
+ if (m_active) {
+ setState(Error, m_socket.errorString());
+ close();
+ }
+}
+
+void NexusSSOLogin::onSslErrors(const QList<QSslError>& errors)
+{
+ if (m_active) {
+ for (const auto& e : errors) {
+ setState(Error, e.errorString());
+ }
+ }
+}
+
+void NexusSSOLogin::onTimeout()
+{
+ abort();
+ setState(Timeout);
+}
+
+
+NexusKeyValidator::NexusKeyValidator(NXMAccessManager& am)
+ : m_manager(am), m_reply(nullptr), m_active(false)
+{
+ m_timeout.setInterval(NXMAccessManager::ValidationTimeout);
+ m_timeout.setSingleShot(true);
+
+ QObject::connect(&m_timeout, &QTimer::timeout, [&]{ onTimeout(); });
+}
+
+NexusKeyValidator::~NexusKeyValidator()
+{
+ abort();
+}
+
+QString NexusKeyValidator::stateToString(States s, const QString& e)
+{
+ 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");
+
+ case NexusKeyValidator::Cancelled:
+ return QObject::tr("Cancelled");
+
+ case NexusKeyValidator::Error: // fall-through
+ default:
+ {
+ if (e.isEmpty()) {
+ return QString("%1").arg(s);
+ } else {
+ return e;
+ }
+ }
+ }
}
+void NexusKeyValidator::start(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::ContentTypeHeader, "application/json");
+ request.setRawHeader("Protocol-Version", "1.0.0");
+ request.setRawHeader("Application-Name", "MO2");
+ request.setRawHeader("Application-Version", m_manager.MOVersion().toUtf8());
+
+ m_reply = m_manager.get(request);
+ if (!m_reply) {
+ close();
+ setState(Error, QObject::tr("Failed to request %1").arg(requestUrl));
+ return;
+ }
+
+ m_timeout.start(NXMAccessManager::ValidationTimeout);
+
+ QObject::connect(
+ m_reply, &QNetworkReply::finished,
+ [&]{ onFinished(); });
+
+ QObject::connect(
+ m_reply, &QNetworkReply::sslErrors,
+ [&](auto&& errors){ onSslErrors(errors); });
+}
+
+void NexusKeyValidator::cancel()
+{
+ if (m_active) {
+ abort();
+ setState(Cancelled);
+ }
+}
+
+bool NexusKeyValidator::isActive() const
+{
+ return m_active;
+}
+
+void NexusKeyValidator::close()
+{
+ m_active = false;
+ m_timeout.stop();
+
+ if (m_reply) {
+ m_reply->disconnect();
+ m_reply->deleteLater();
+ m_reply = nullptr;
+ }
+}
+
+void NexusKeyValidator::abort()
+{
+ m_active = false;
+ m_timeout.stop();
+
+ if (m_reply) {
+ m_reply->disconnect();
+ m_reply->abort();
+ m_reply->deleteLater();
+ m_reply = nullptr;
+ }
+}
+
+void NexusKeyValidator::setState(States s, const QString& error)
+{
+ if (stateChanged) {
+ stateChanged(s, error);
+ }
+}
+
+void NexusKeyValidator::onFinished()
+{
+ if (!m_reply) {
+ // shouldn't happen
+ return;
+ }
+
+ m_timeout.stop();
+
+ 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 QJsonObject data = doc.object();
+
+ if (code != 200) {
+ handleError(code, data.value("message").toString(), error);
+ return;
+ }
+
+ if (doc.isNull()) {
+ setState(InvalidJson);
+ return;
+ }
+
+ if (!data.contains("user_id")) {
+ setState(BadResponse);
+ return;
+ }
+
+ const int id = data.value("user_id").toInt();
+ const QString key = data.value("key").toString();
+ const QString name = data.value("name").toString();
+ const bool premium = data.value("is_premium").toBool();
+
+ const auto user = APIUserAccount()
+ .apiKey(key)
+ .id(QString("%1").arg(id))
+ .name(name)
+ .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular)
+ .limits(NexusInterface::parseLimits(headers));
+
+ if (finished) {
+ setState(Finished);
+ finished(user);
+ }
+}
+
+void NexusKeyValidator::onSslErrors(const QList<QSslError>& errors)
+{
+ if (m_active) {
+ for (const auto& e : errors) {
+ setState(Error, e.errorString());
+ }
+ }
+}
+
+void NexusKeyValidator::onTimeout()
+{
+ abort();
+ setState(Timeout);
+}
+
+void NexusKeyValidator::handleError(
+ int code, const QString& nexusMessage, const QString& httpError)
+{
+ QString s = httpError;
+
+ if (!nexusMessage.isEmpty()) {
+ if (!s.isEmpty()) {
+ s += ", ";
+ }
+
+ s += nexusMessage;
+ }
+
+ if (code != 0) {
+ if (s.isEmpty()) {
+ s = QString("HTTP code %1").arg(code);
+ } else {
+ s += QString(" (%1)").arg(code);
+ }
+ }
+
+ setState(Error, s);
+}
+
+
+
NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion)
: QNetworkAccessManager(parent)
- , m_ValidateReply(nullptr)
+ , m_ProgressDialog(new ValidationProgressDialog(ValidationTimeout))
, m_MOVersion(moVersion)
+ , m_validator(*this)
+ , m_validationState(NotChecked)
{
- m_ValidateTimeout.setSingleShot(true);
- m_ValidateTimeout.setInterval(30000);
- connect(&m_ValidateTimeout, SIGNAL(timeout()), this, SLOT(validateTimeout()));
- setCookieJar(new PersistentCookieJar(
- QDir::fromNativeSeparators(Settings::instance().getCacheDirectory() + "/nexus_cookies.dat")));
+ m_validator.stateChanged = [&](auto&& s, auto&& e){ onValidatorState(s, e); };
+ m_validator.finished = [&](auto&& user){ onValidatorFinished(user); };
+
+ setCookieJar(new PersistentCookieJar(QDir::fromNativeSeparators(
+ Settings::instance().getCacheDirectory() + "/nexus_cookies.dat")));
if (networkAccessible() == QNetworkAccessManager::UnknownAccessibility) {
// why is this necessary all of a sudden?
@@ -62,12 +571,9 @@ NXMAccessManager::NXMAccessManager(QObject *parent, const QString &moVersion)
}
}
-NXMAccessManager::~NXMAccessManager()
+void NXMAccessManager::setTopLevelWidget(QWidget* w)
{
- if (m_ValidateReply != nullptr) {
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
- }
+ m_ProgressDialog->setParentWidget(w);
}
QNetworkReply *NXMAccessManager::createRequest(
@@ -90,10 +596,9 @@ QNetworkReply *NXMAccessManager::createRequest(
}
}
-
void NXMAccessManager::showCookies() const
{
- QUrl url(nexusBaseUrl + "/");
+ QUrl url(NexusBaseUrl + "/");
for (const QNetworkCookie &cookie : cookieJar()->cookiesForUrl(url)) {
qDebug("%s - %s (expires: %s)",
cookie.name().constData(), cookie.value().constData(),
@@ -111,90 +616,81 @@ void NXMAccessManager::clearCookies()
}
}
-void NXMAccessManager::startValidationCheck()
+void NXMAccessManager::startValidationCheck(const QString& key)
{
- qDebug("Checking Nexus API Key...");
- QString requestString = nexusBaseUrl + "/users/validate";
-
- QNetworkRequest request(requestString);
- request.setRawHeader("APIKEY", m_ApiKey.toUtf8());
- request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, 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_MOVersion.toUtf8());
+ m_validationState = NotChecked;
+ m_validator.start(key);
+ m_ProgressDialog->start();
+}
- m_ProgressDialog = new QProgressDialog(nullptr);
- m_ProgressDialog->setLabelText(tr("Validating Nexus Connection"));
- QList<QPushButton*> buttons = m_ProgressDialog->findChildren<QPushButton*>();
- buttons.at(0)->setEnabled(false);
- m_ProgressDialog->show();
- QCoreApplication::processEvents(); // for some reason the whole app hangs during the login. This way the user has at least a little feedback
+void NXMAccessManager::onValidatorState(
+ NexusKeyValidator::States s, const QString& e)
+{
+ if (s == NexusKeyValidator::Connecting || s == NexusKeyValidator::Finished) {
+ // no-op, success is handled in onValidatorFinished()
+ return;
+ }
- m_ValidateReply = get(request);
- m_ValidateTimeout.start();
- m_ValidateState = VALIDATE_CHECKING;
- connect(m_ValidateReply, SIGNAL(finished()), this, SLOT(validateFinished()));
- connect(m_ValidateReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(validateError(QNetworkReply::NetworkError)));
+ m_ProgressDialog->stop();
+ m_validationState = Invalid;
+ emit validateFailed(NexusKeyValidator::stateToString(s, e));
}
+void NXMAccessManager::onValidatorFinished(const APIUserAccount& user)
+{
+ m_ProgressDialog->stop();
+
+ m_validationState = Valid;
+ emit credentialsReceived(user);
+ emit validateSuccessful(true);
+}
bool NXMAccessManager::validated() const
{
- if (m_ValidateState == VALIDATE_CHECKING) {
- QProgressDialog progress;
- progress.setLabelText(tr("Validating Nexus Connection"));
- QList<QPushButton*> buttons = m_ProgressDialog->findChildren<QPushButton*>();
- buttons.at(0)->setEnabled(false);
- progress.show();
- while (m_ValidateState == VALIDATE_CHECKING) {
- QCoreApplication::processEvents();
- QThread::msleep(100);
- }
- progress.hide();
+ if (m_validator.isActive()) {
+ m_ProgressDialog->show();
}
- return m_ValidateState == VALIDATE_VALID;
+ return (m_validationState == Valid);
}
-
void NXMAccessManager::refuseValidation()
{
- m_ValidateState = VALIDATE_REFUSED;
+ m_validationState = Invalid;
}
-
bool NXMAccessManager::validateAttempted() const
{
- return m_ValidateState != VALIDATE_NOT_CHECKED;
+ return (m_validationState != NotChecked);
}
-
bool NXMAccessManager::validateWaiting() const
{
- return m_ValidateReply != nullptr;
+ return m_validator.isActive();
}
-
void NXMAccessManager::apiCheck(const QString &apiKey, bool force)
{
- if (m_ValidateReply != nullptr) {
+ if (m_validator.isActive()) {
return;
}
if (force) {
- m_ValidateState = VALIDATE_NOT_CHECKED;
+ m_validationState = NotChecked;
}
- if (m_ValidateState == VALIDATE_VALID) {
+ if (m_validationState == Valid) {
emit validateSuccessful(false);
return;
}
- m_ApiKey = apiKey;
- startValidationCheck();
+ startValidationCheck(apiKey);
}
+const QString& NXMAccessManager::MOVersion() const
+{
+ return m_MOVersion;
+}
QString NXMAccessManager::userAgent(const QString &subModule) const
{
@@ -213,109 +709,8 @@ QString NXMAccessManager::userAgent(const QString &subModule) const
return QString("Mod Organizer/%1 (%2) Qt/%3").arg(m_MOVersion, comments.join("; "), qVersion());
}
-
-QString NXMAccessManager::apiKey() const
-{
- return m_ApiKey;
-}
-
void NXMAccessManager::clearApiKey()
{
- m_ApiKey = "";
- m_ValidateState = VALIDATE_NOT_VALID;
-
+ m_validator.cancel();
emit credentialsReceived(APIUserAccount());
}
-
-void NXMAccessManager::validateTimeout()
-{
- m_ValidateTimeout.stop();
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->hide();
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
- }
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_VALID;
-
- if (m_ValidateReply != nullptr) {
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
- }
-
- emit validateFailed(tr("There was a timeout during the request"));
-}
-
-
-void NXMAccessManager::validateError(QNetworkReply::NetworkError)
-{
- m_ValidateTimeout.stop();
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->hide();
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
- }
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_VALID;
-
- if (m_ValidateReply != nullptr) {
- m_ValidateReply->disconnect();
- QString error = m_ValidateReply->errorString();
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
- emit validateFailed(error);
- } else {
- emit validateFailed(tr("Unknown error"));
- }
-}
-
-
-void NXMAccessManager::validateFinished()
-{
- m_ValidateTimeout.stop();
- if (m_ProgressDialog != nullptr) {
- m_ProgressDialog->deleteLater();
- m_ProgressDialog = nullptr;
- }
-
- if (m_ValidateReply != nullptr) {
- QJsonDocument jdoc = QJsonDocument::fromJson(m_ValidateReply->readAll());
- if (!jdoc.isNull()) {
- QJsonObject credentialsData = jdoc.object();
- if (credentialsData.contains("user_id")) {
- int id = credentialsData.value("user_id").toInt();
- QString name = credentialsData.value("name").toString();
- bool premium = credentialsData.value("is_premium").toBool();
-
- const auto user = APIUserAccount()
- .id(QString("%1").arg(id))
- .name(name)
- .type(premium ? APIUserAccountTypes::Premium : APIUserAccountTypes::Regular)
- .limits(NexusInterface::parseLimits(m_ValidateReply));
-
-
- emit credentialsReceived(user);
-
- m_ValidateReply->deleteLater();
- m_ValidateReply = nullptr;
-
- m_ValidateState = VALIDATE_VALID;
- emit validateSuccessful(true);
-
- } else {
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_VALID;
- emit validateFailed(tr("Validation failed, please reauthenticate in the Settings -> Nexus tab: %1").arg(credentialsData.value("message").toString()));
- }
- } else {
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_CHECKED;
- emit validateFailed(tr("Could not parse response. Invalid JSON."));
- }
- }
- else {
- m_ApiKey.clear();
- m_ValidateState = VALIDATE_NOT_CHECKED;
- emit validateFailed(tr("Unknown error."));
- }
-}
diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h
index 1bdeae40..eed7c1c9 100644
--- a/src/nxmaccessmanager.h
+++ b/src/nxmaccessmanager.h
@@ -25,9 +25,137 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include <QTimer>
#include <QNetworkReply>
#include <QProgressDialog>
+#include <QElapsedTimer>
+#include <QDialogButtonBox>
+#include <QWebSocket>
#include <set>
namespace MOBase { class IPluginGame; }
+class NXMAccessManager;
+
+class ValidationProgressDialog : private QDialog
+{
+ Q_OBJECT;
+
+public:
+ ValidationProgressDialog(std::chrono::seconds timeout);
+
+ void setParentWidget(QWidget* w);
+
+ void start();
+ void stop();
+
+ using QDialog::show;
+
+protected:
+ void closeEvent(QCloseEvent* e) override;
+
+private:
+ std::chrono::seconds m_timeout;
+ QProgressBar* m_bar;
+ QDialogButtonBox* m_buttons;
+ QTimer* m_timer;
+ QElapsedTimer m_elapsed;
+
+ void onButton(QAbstractButton* b);
+ void onTimer();
+};
+
+
+class NexusSSOLogin
+{
+public:
+ enum States
+ {
+ ConnectingToSSO,
+ WaitingForToken,
+ WaitingForBrowser,
+ Finished,
+ Timeout,
+ ClosedByRemote,
+ Cancelled,
+ Error
+ };
+
+ std::function<void (QString)> keyChanged;
+ std::function<void (States, QString)> stateChanged;
+
+ static QString stateToString(States s, const QString& e);
+
+ NexusSSOLogin();
+
+ void start();
+ void cancel();
+
+ bool isActive() const;
+
+private:
+ QWebSocket m_socket;
+ QString m_guid;
+ bool m_keyReceived;
+ bool m_active;
+ QTimer m_timeout;
+
+ void setState(States s, const QString& error={});
+
+ void close();
+ void abort();
+
+ void onConnected();
+ void onMessage(const QString& s);
+ void onDisconnected();
+ void onError(QAbstractSocket::SocketError e);
+ void onSslErrors(const QList<QSslError>& errors);
+ void onTimeout();
+};
+
+
+class NexusKeyValidator
+{
+public:
+ enum States
+ {
+ Connecting,
+ Finished,
+ InvalidJson,
+ BadResponse,
+ Timeout,
+ Cancelled,
+ Error
+ };
+
+ std::function<void (APIUserAccount)> finished;
+ std::function<void (States, QString)> stateChanged;
+
+ static QString stateToString(States s, const QString& e);
+
+ NexusKeyValidator(NXMAccessManager& am);
+ ~NexusKeyValidator();
+
+ void start(const QString& key);
+ void cancel();
+
+ bool isActive() const;
+
+private:
+ NXMAccessManager& m_manager;
+ QNetworkReply* m_reply;
+ QTimer m_timeout;
+ bool m_active;
+
+ void setState(States s, const QString& error={});
+
+ void close();
+ void abort();
+
+ void onFinished();
+ void onSslErrors(const QList<QSslError>& errors);
+ void onTimeout();
+
+ void handleError(
+ int code, const QString& nexusMessage, const QString& httpError);
+};
+
/**
* @brief access manager extended to handle nxm links
@@ -36,10 +164,12 @@ class NXMAccessManager : public QNetworkAccessManager
{
Q_OBJECT
public:
+ static const std::chrono::seconds ValidationTimeout;
explicit NXMAccessManager(QObject *parent, const QString &moVersion);
- ~NXMAccessManager();
+
+ void setTopLevelWidget(QWidget* w);
bool validated() const;
@@ -53,12 +183,10 @@ public:
void clearCookies();
QString userAgent(const QString &subModule = QString()) const;
+ const QString& MOVersion() const;
- QString apiKey() const;
void clearApiKey();
- void startValidationCheck();
-
void refuseValidation();
signals:
@@ -76,17 +204,9 @@ signals:
* @param necessary true if a login was necessary and succeeded, false if the user is still logged in
**/
void validateSuccessful(bool necessary);
-
void validateFailed(const QString &message);
-
void credentialsReceived(const APIUserAccount& user);
-private slots:
-
- void validateFinished();
- void validateError(QNetworkReply::NetworkError errorCode);
- void validateTimeout();
-
protected:
virtual QNetworkReply *createRequest(
@@ -94,22 +214,22 @@ protected:
QIODevice *device);
private:
- QTimer m_ValidateTimeout;
- QNetworkReply *m_ValidateReply;
- QProgressDialog *m_ProgressDialog { nullptr };
+ enum States
+ {
+ NotChecked,
+ Valid,
+ Invalid
+ };
+ QWidget* m_TopLevel;
+ mutable ValidationProgressDialog* m_ProgressDialog;
QString m_MOVersion;
+ NexusKeyValidator m_validator;
+ States m_validationState;
- QString m_ApiKey;
-
- enum {
- VALIDATE_NOT_CHECKED,
- VALIDATE_CHECKING,
- VALIDATE_NOT_VALID,
- VALIDATE_ATTEMPT_FAILED,
- VALIDATE_REFUSED,
- VALIDATE_VALID
- } m_ValidateState = VALIDATE_NOT_CHECKED;
+ void startValidationCheck(const QString& key);
+ void onValidatorState(NexusKeyValidator::States s, const QString& e);
+ void onValidatorFinished(const APIUserAccount& user);
};
#endif // NXMACCESSMANAGER_H
diff --git a/src/organizercore.cpp b/src/organizercore.cpp
index 81ec7f43..eeb69e61 100644
--- a/src/organizercore.cpp
+++ b/src/organizercore.cpp
@@ -2484,6 +2484,9 @@ void OrganizerCore::loginSuccessfulUpdate(bool necessary)
void OrganizerCore::loginFailed(const QString &message)
{
+ qCritical().nospace().noquote()
+ << "Nexus API validation failed: " << message;
+
if (QMessageBox::question(qApp->activeWindow(), tr("Login failed"),
tr("Login failed, try again?"))
== QMessageBox::Yes) {
diff --git a/src/pch.h b/src/pch.h
index 1d8df43a..955939ba 100644
--- a/src/pch.h
+++ b/src/pch.h
@@ -98,6 +98,7 @@
#include <QDirIterator>
#include <QDragEnterEvent>
#include <QDropEvent>
+#include <QElapsedTimer>
#include <QEvent>
#include <QFile>
#include <QFileDialog>
diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp
index 271c621b..e967b27c 100644
--- a/src/selfupdater.cpp
+++ b/src/selfupdater.cpp
@@ -130,7 +130,7 @@ void SelfUpdater::testForUpdate()
m_GitHub.releases(GitHub::Repository("Modorganizer2", "modorganizer"),
[this](const QJsonArray &releases) {
if (releases.isEmpty()) {
- qDebug("Unable to connect to github.com to check version");
+ // error message already logged
return;
}
diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp
index 95e4ceb0..0dae31ac 100644
--- a/src/settingsdialog.cpp
+++ b/src/settingsdialog.cpp
@@ -28,7 +28,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#include "settings.h"
#include "instancemanager.h"
#include "nexusinterface.h"
-#include "nxmaccessmanager.h"
#include "plugincontainer.h"
#include <boost/uuid/uuid_generators.hpp>
@@ -49,7 +48,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
using namespace MOBase;
-
class NexusManualKeyDialog : public QDialog
{
public:
@@ -57,6 +55,7 @@ public:
: QDialog(parent), ui(new Ui::NexusManualKeyDialog)
{
ui->setupUi(this);
+ ui->key->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
connect(ui->openBrowser, &QPushButton::clicked, [&]{ openBrowser(); });
connect(ui->paste, &QPushButton::clicked, [&]{ paste(); });
@@ -103,30 +102,21 @@ SettingsDialog::SettingsDialog(PluginContainer *pluginContainer, Settings* setti
, ui(new Ui::SettingsDialog)
, m_settings(settings)
, m_PluginContainer(pluginContainer)
- , m_nexusLogin(new QWebSocket)
- , m_KeyReceived(false)
- , m_KeyCleared(false)
+ , m_keyChanged(false)
, m_GeometriesReset(false)
{
ui->setupUi(this);
ui->pluginSettingsList->setStyleSheet("QTreeWidget::item {padding-right: 10px;}");
- QShortcut *delShortcut
- = new QShortcut(QKeySequence(Qt::Key_Delete), ui->pluginBlacklist);
+ QShortcut *delShortcut = new QShortcut(
+ QKeySequence(Qt::Key_Delete), ui->pluginBlacklist);
connect(delShortcut, SIGNAL(activated()), this, SLOT(deleteBlacklistItem()));
- connect(m_nexusLogin, SIGNAL(connected()), this, SLOT(dispatchLogin()));
- connect(m_nexusLogin, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(authError(QAbstractSocket::SocketError)));
- connect(m_nexusLogin, SIGNAL(textMessageReceived(const QString &)), this, SLOT(receiveApiKey(const QString &)));
- connect(m_nexusLogin, SIGNAL(disconnected()), this, SLOT(completeApiConnection()));
- m_loginTimer.callOnTimeout(this, &SettingsDialog::loginPing);
- updateNexusButtons();
+ updateNexusState();
}
SettingsDialog::~SettingsDialog()
{
- m_loginTimer.stop();
- m_nexusLogin->close();
disconnect(this);
delete ui;
}
@@ -188,7 +178,7 @@ bool SettingsDialog::getResetGeometries()
bool SettingsDialog::getApiKeyChanged()
{
- return m_KeyReceived || m_KeyCleared;
+ return m_keyChanged;
}
void SettingsDialog::on_categoriesBtn_clicked()
@@ -391,167 +381,218 @@ void SettingsDialog::on_resetDialogsButton_clicked()
void SettingsDialog::on_nexusConnect_clicked()
{
- fetchNexusApiKey();
+ if (m_nexusLogin && m_nexusLogin->isActive()) {
+ m_nexusLogin->cancel();
+ return;
+ }
+
+ if (!m_nexusLogin) {
+ m_nexusLogin.reset(new NexusSSOLogin);
+
+ m_nexusLogin->keyChanged = [&](auto&& s){
+ onSSOKeyChanged(s);
+ };
+
+ m_nexusLogin->stateChanged = [&](auto&& s, auto&& e){
+ onSSOStateChanged(s, e);
+ };
+ }
+
+ ui->nexusLog->clear();
+ m_nexusLogin->start();
+ updateNexusState();
}
void SettingsDialog::on_nexusManualKey_clicked()
{
- NexusManualKeyDialog dialog(this);
+ if (m_nexusValidator && m_nexusValidator->isActive()) {
+ m_nexusValidator->cancel();
+ return;
+ }
+ NexusManualKeyDialog dialog(this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
const auto key = dialog.key();
-
if (key.isEmpty()) {
clearKey();
- } else {
- if (setKey(key)) {
- testApiKey();
- }
+ return;
}
+
+ ui->nexusLog->clear();
+ validateKey(key);
}
-void SettingsDialog::fetchNexusApiKey()
+void SettingsDialog::on_nexusDisconnect_clicked()
{
- QUrl url = QUrl("wss://sso.nexusmods.com");
- m_nexusLogin->open(url);
- updateNexusButtons();
+ clearKey();
+ ui->nexusLog->clear();
+ addNexusLog(tr("Disconnected."));
}
-void SettingsDialog::dispatchLogin()
+void SettingsDialog::validateKey(const QString& key)
{
- m_KeyReceived = false;
- QJsonObject login;
- if (m_UUID.isEmpty()) {
- boost::uuids::random_generator generator;
- boost::uuids::uuid sessionId = generator();
- m_UUID = boost::uuids::to_string(sessionId).c_str();
+ if (!m_nexusValidator) {
+ m_nexusValidator.reset(new NexusKeyValidator(
+ *NexusInterface::instance(m_PluginContainer)->getAccessManager()));
+
+ m_nexusValidator->stateChanged = [&](auto&& s, auto&& e){
+ onValidatorStateChanged(s, e);
+ };
+
+ m_nexusValidator->finished = [&](auto&& user) {
+ onValidatorFinished(user);
+ };
}
- login.insert(QString("id"), QJsonValue(m_UUID));
- login.insert(QString("token"), QJsonValue(m_AuthToken));
- login.insert(QString("protocol"), 2);
- QJsonDocument loginDoc(login);
- QString finalMessage(loginDoc.toJson());
- m_nexusLogin->sendTextMessage(finalMessage);
- QDesktopServices::openUrl(QUrl(QString("https://www.nexusmods.com/sso?id=%1&application=%2").arg(m_UUID).arg("modorganizer2")));
- m_loginTimer.start(30000);
+
+ addNexusLog(tr("Checking API key..."));
+ m_nexusValidator->start(key);
}
-void SettingsDialog::loginPing()
+void SettingsDialog::onSSOKeyChanged(const QString& key)
{
- if (m_nexusLogin->isValid()) {
- m_nexusLogin->ping();
- m_totalPings++;
- }
- if (m_totalPings >= 60) {
- m_loginTimer.stop();
- m_totalPings = 0;
- m_nexusLogin->close(QWebSocketProtocol::CloseCodeGoingAway, "Timeout: No response received after thirty minutes. Cancelling request.");
+ if (key.isEmpty()) {
+ clearKey();
+ } else {
+ addNexusLog(tr("Received API key."));
+ validateKey(key);
}
}
-void SettingsDialog::authError(QAbstractSocket::SocketError error)
+void SettingsDialog::onSSOStateChanged(NexusSSOLogin::States s, const QString& e)
{
- auto errorInfo = m_nexusLogin->errorString();
- qCritical() << "An error occurred: " << errorInfo;
+ if (s != NexusSSOLogin::Finished) {
+ // finished state is handled in onSSOKeyChanged()
+ const auto log = NexusSSOLogin::stateToString(s, e);
+
+ for (auto&& line : log.split("\n")) {
+ addNexusLog(line);
+ }
+ }
+
+ updateNexusState();
}
-void SettingsDialog::receiveApiKey(const QString &response)
+void SettingsDialog::onValidatorStateChanged(
+ NexusKeyValidator::States s, const QString& e)
{
- QJsonDocument responseDoc = QJsonDocument::fromJson(response.toUtf8());
- QVariantMap responseData = responseDoc.object().toVariantMap();
- if (responseData["success"].toBool()) {
- QVariantMap data = responseData["data"].toMap();
- if (data.contains("connection_token")) {
- m_AuthToken = data["connection_token"].toString();
- } else {
- const auto key = data["api_key"].toString();
-
- m_nexusLogin->close();
- m_loginTimer.stop();
- m_totalPings = 0;
+ if (s != NexusKeyValidator::Finished) {
+ // finished state is handled in onValidatorFinished()
+ const auto log = NexusKeyValidator::stateToString(s, e);
- if (key.isEmpty()) {
- clearKey();
- } else {
- setKey(key);
- }
+ for (auto&& line : log.split("\n")) {
+ addNexusLog(line);
}
- } else {
- QString error("There was a problem with SSO initialization: %1");
- qCritical() << error.arg(responseData["error"].toString());
- m_nexusLogin->close();
}
+
+ updateNexusState();
}
-void SettingsDialog::completeApiConnection()
+void SettingsDialog::onValidatorFinished(const APIUserAccount& user)
{
- if (!m_KeyReceived && !m_loginTimer.isActive()) {
- QMessageBox::warning(qApp->activeWindow(), tr("Error"),
- tr("Failed to retrieve a Nexus API key! Please try again. "
- "A browser window should open asking you to authorize."));
+ NexusInterface::instance(m_PluginContainer)->setUserAccount(user);
- // try again
- fetchNexusApiKey();
+ if (!user.apiKey().isEmpty()) {
+ if (setKey(user.apiKey())) {
+ addNexusLog(tr("Linked with Nexus successfully."));
+ }
}
}
+void SettingsDialog::addNexusLog(const QString& s)
+{
+ ui->nexusLog->addItem(s);
+ ui->nexusLog->scrollToBottom();
+}
+
bool SettingsDialog::setKey(const QString& key)
{
- m_KeyReceived = true;
+ m_keyChanged = true;
const bool ret = m_settings->setNexusApiKey(key);
- updateNexusButtons();
+ updateNexusState();
return ret;
}
bool SettingsDialog::clearKey()
{
- m_KeyCleared = true;
+ m_keyChanged = true;
const auto ret = m_settings->clearNexusApiKey();
- updateNexusButtons();
NexusInterface::instance(m_PluginContainer)->getAccessManager()->clearApiKey();
+ updateNexusState();
return ret;
}
-void SettingsDialog::testApiKey()
+void SettingsDialog::updateNexusState()
{
- QString key;
- if (!m_settings->getNexusApiKey(key)) {
- qWarning().nospace() << "can't test API key, nothing stored";
- return;
- }
-
- NexusInterface::instance(m_PluginContainer)->getAccessManager()->apiCheck(key, true);
+ updateNexusButtons();
+ updateNexusData();
}
void SettingsDialog::updateNexusButtons()
{
- if (m_nexusLogin->state() != QAbstractSocket::UnconnectedState) {
+ if (m_nexusLogin && m_nexusLogin->isActive()) {
// api key is in the process of being retrieved
- ui->nexusConnect->setText("Connecting the API. Please login within the browser and accept the request. This will time out after 30 minutes.");
- ui->nexusConnect->setEnabled(false);
+ ui->nexusConnect->setText(tr("Cancel"));
+ ui->nexusConnect->setEnabled(true);
ui->nexusDisconnect->setEnabled(false);
+ ui->nexusManualKey->setText(tr("Enter API Key Manually"));
ui->nexusManualKey->setEnabled(false);
}
+ else if (m_nexusValidator && m_nexusValidator->isActive()) {
+ // api key is in the process of being tested
+ ui->nexusConnect->setText(tr("Connect to Nexus"));
+ ui->nexusConnect->setEnabled(false);
+ ui->nexusDisconnect->setEnabled(false);
+ ui->nexusManualKey->setText(tr("Cancel"));
+ ui->nexusManualKey->setEnabled(true);
+ }
else if (m_settings->hasNexusApiKey()) {
// api key is present
- ui->nexusConnect->setText("Nexus API Key Stored");
+ ui->nexusConnect->setText(tr("Connect to Nexus"));
ui->nexusConnect->setEnabled(false);
ui->nexusDisconnect->setEnabled(true);
+ ui->nexusManualKey->setText(tr("Enter API Key Manually"));
ui->nexusManualKey->setEnabled(false);
} else {
// api key not present
- ui->nexusConnect->setText("Connect to Nexus");
+ ui->nexusConnect->setText(tr("Connect to Nexus"));
ui->nexusConnect->setEnabled(true);
ui->nexusDisconnect->setEnabled(false);
+ ui->nexusManualKey->setText(tr("Enter API Key Manually"));
ui->nexusManualKey->setEnabled(true);
}
}
+void SettingsDialog::updateNexusData()
+{
+ const auto user = NexusInterface::instance(m_PluginContainer)
+ ->getAPIUserAccount();
+
+ if (user.isValid()) {
+ ui->nexusUserID->setText(user.id());
+ ui->nexusName->setText(user.name());
+ ui->nexusAccount->setText(localizedUserAccountType(user.type()));
+
+ ui->nexusDailyRequests->setText(QString("%1/%2")
+ .arg(user.limits().remainingDailyRequests)
+ .arg(user.limits().maxDailyRequests));
+
+ ui->nexusHourlyRequests->setText(QString("%1/%2")
+ .arg(user.limits().remainingHourlyRequests)
+ .arg(user.limits().maxHourlyRequests));
+ } else {
+ ui->nexusUserID->setText(tr("N/A"));
+ ui->nexusName->setText(tr("N/A"));
+ ui->nexusAccount->setText(tr("N/A"));
+ ui->nexusDailyRequests->setText(tr("N/A"));
+ ui->nexusHourlyRequests->setText(tr("N/A"));
+ }
+}
+
void SettingsDialog::storeSettings(QListWidgetItem *pluginItem)
{
if (pluginItem != nullptr) {
@@ -619,11 +660,6 @@ void SettingsDialog::on_clearCacheButton_clicked()
NexusInterface::instance(m_PluginContainer)->clearCache();
}
-void SettingsDialog::on_nexusDisconnect_clicked()
-{
- clearKey();
-}
-
void SettingsDialog::normalizePath(QLineEdit *lineEdit)
{
QString text = lineEdit->text();
diff --git a/src/settingsdialog.h b/src/settingsdialog.h
index 858a36d4..c5f487fd 100644
--- a/src/settingsdialog.h
+++ b/src/settingsdialog.h
@@ -21,12 +21,9 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
#define SETTINGSDIALOG_H
#include "tutorabledialog.h"
+#include "nxmaccessmanager.h"
#include <iplugin.h>
-#include <QComboBox>
-#include <QDialog>
-#include <QWebSocket>
#include <QListWidgetItem>
-#include <QTimer>
class PluginContainer;
class Settings;
@@ -35,6 +32,7 @@ namespace Ui {
class SettingsDialog;
}
+
/**
* dialog used to change settings for Mod Organizer. On top of the
* settings managed by the "Settings" class, this offers a button to open the
@@ -127,11 +125,6 @@ private slots:
void on_resetGeometryBtn_clicked();
void deleteBlacklistItem();
- void dispatchLogin();
- void loginPing();
- void authError(QAbstractSocket::SocketError error);
- void receiveApiKey(const QString &apiKey);
- void completeApiConnection();
private:
Ui::SettingsDialog *ui;
@@ -145,23 +138,28 @@ private:
QColor m_ContainsColor;
QColor m_ContainedColor;
- bool m_KeyReceived;
- bool m_KeyCleared;
bool m_GeometriesReset;
- QString m_UUID;
- QString m_AuthToken;
+ bool m_keyChanged;
QString m_ExecutableBlacklist;
- QWebSocket *m_nexusLogin;
- QTimer m_loginTimer;
- int m_totalPings = 0;
+ std::unique_ptr<NexusSSOLogin> m_nexusLogin;
+ std::unique_ptr<NexusKeyValidator> m_nexusValidator;
+ void validateKey(const QString& key);
bool setKey(const QString& key);
bool clearKey();
+
+ void updateNexusState();
void updateNexusButtons();
+ void updateNexusData();
+
+ void onSSOKeyChanged(const QString& key);
+ void onSSOStateChanged(NexusSSOLogin::States s, const QString& e);
+
+ void onValidatorStateChanged(NexusKeyValidator::States s, const QString& e);
+ void onValidatorFinished(const APIUserAccount& user);
- void fetchNexusApiKey();
- void testApiKey();
+ void addNexusLog(const QString& s);
};
#endif // SETTINGSDIALOG_H
diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui
index faaf1653..fccc8be0 100644
--- a/src/settingsdialog.ui
+++ b/src/settingsdialog.ui
@@ -451,112 +451,237 @@ If you use pre-releases, never contact me directly by e-mail or via private mess
<attribute name="title">
<string>Nexus</string>
</attribute>
- <layout class="QVBoxLayout" name="verticalLayout_4">
+ <layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,1,0,0,0,1">
<item>
- <widget class="QGroupBox" name="nexusBox">
- <property name="toolTip">
- <string>Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things.</string>
- </property>
- <property name="whatsThis">
- <string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
-&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
-p, li { white-space: pre-wrap; }
-&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;&quot;&gt;
-&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Allows Mod Organizer to connect to the Nexus for downloading mods, checking for updates, and other such things. Clicking &amp;quot;Connect to Nexus&amp;quot; will open a Nexus webpage to authorise Mod Organizer. You will need to be logged into your Nexus account. The authorisation is stored in the Windows Credential Manager. Your Nexus username and password are not required or stored by Mod Organizer.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
- </property>
+ <widget class="QWidget" name="widget" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_5">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QGroupBox" name="groupBox_2">
+ <property name="title">
+ <string>Nexus Account</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>User ID:</string>
+ </property>
+ <property name="textInteractionFlags">
+ <set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nexusUserID">
+ <property name="text">
+ <string>id</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="text">
+ <string>Name:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="nexusName">
+ <property name="text">
+ <string>name</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_11">
+ <property name="text">
+ <string>Account:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="nexusAccount">
+ <property name="text">
+ <string>account</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_3">
+ <property name="title">
+ <string>Statistics</string>
+ </property>
+ <layout class="QFormLayout" name="formLayout_3">
+ <property name="horizontalSpacing">
+ <number>10</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label_30">
+ <property name="text">
+ <string>Daily requests:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="nexusDailyRequests">
+ <property name="text">
+ <string>daily requests</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_31">
+ <property name="text">
+ <string>Hourly requests:</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="nexusHourlyRequests">
+ <property name="text">
+ <string>hourly requests</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QGroupBox" name="groupBox_4">
<property name="title">
- <string>Nexus</string>
+ <string>Nexus Connection</string>
</property>
- <layout class="QVBoxLayout" name="verticalLayout_2">
+ <layout class="QHBoxLayout" name="horizontalLayout_14">
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_10">
- <item>
- <widget class="QPushButton" name="nexusConnect">
- <property name="text">
- <string>Connect to Nexus</string>
- </property>
- </widget>
- </item>
- </layout>
+ <widget class="QWidget" name="widget_2" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="nexusConnect">
+ <property name="text">
+ <string>Connect to Nexus</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="nexusManualKey">
+ <property name="toolTip">
+ <string>Manually enter the API key and try to login</string>
+ </property>
+ <property name="text">
+ <string>Enter API Key Manually</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="nexusDisconnect">
+ <property name="toolTip">
+ <string>Clear the stored Nexus API key and force reauthorization.</string>
+ </property>
+ <property name="text">
+ <string>Disconnect from Nexus</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
</item>
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_11">
- <item>
- <spacer name="horizontalSpacer_7">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- <item>
- <widget class="QPushButton" name="nexusManualKey">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="toolTip">
- <string>Manually enter the API key and try to login</string>
- </property>
- <property name="text">
- <string>Enter API Key Manually</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="nexusDisconnect">
- <property name="toolTip">
- <string>Clear the stored Nexus API key and force reauthorization.</string>
- </property>
- <property name="text">
- <string>Disconnect from Nexus</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QPushButton" name="clearCacheButton">
- <property name="toolTip">
- <string>Remove cache and cookies.</string>
- </property>
- <property name="text">
- <string>Clear Cache</string>
- </property>
- <property name="icon">
- <iconset resource="resources.qrc">
- <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
- </property>
- </widget>
- </item>
- <item>
- <spacer name="horizontalSpacer_5">
- <property name="orientation">
- <enum>Qt::Horizontal</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>40</width>
- <height>20</height>
- </size>
- </property>
- </spacer>
- </item>
- </layout>
+ <widget class="QWidget" name="widget_3" native="true">
+ <layout class="QVBoxLayout" name="verticalLayout_12">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QListWidget" name="nexusLog">
+ <property name="sizeAdjustPolicy">
+ <enum>QAbstractScrollArea::AdjustToContents</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
</item>
</layout>
</widget>
</item>
<item>
+ <widget class="QWidget" name="widget_4" native="true">
+ <layout class="QHBoxLayout" name="horizontalLayout_10">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ </layout>
+ </widget>
+ </item>
+ <item>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="0">
<widget class="QCheckBox" name="offlineBox">
@@ -619,6 +744,20 @@ p, li { white-space: pre-wrap; }
</widget>
</item>
<item>
+ <widget class="QPushButton" name="clearCacheButton">
+ <property name="toolTip">
+ <string>Remove cache and cookies.</string>
+ </property>
+ <property name="text">
+ <string>Clear Cache</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/edit_clear</normaloff>:/MO/gui/edit_clear</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -682,19 +821,6 @@ p, li { white-space: pre-wrap; }
</item>
</layout>
</item>
- <item>
- <spacer name="verticalSpacer_3">
- <property name="orientation">
- <enum>Qt::Vertical</enum>
- </property>
- <property name="sizeHint" stdset="0">
- <size>
- <width>20</width>
- <height>40</height>
- </size>
- </property>
- </spacer>
- </item>
</layout>
</widget>
<widget class="QWidget" name="steamTab">
@@ -1359,7 +1485,6 @@ programs you are intentionally running.</string>
<tabstop>browseProfilesDirBtn</tabstop>
<tabstop>overwriteDirEdit</tabstop>
<tabstop>browseOverwriteDirBtn</tabstop>
- <tabstop>clearCacheButton</tabstop>
<tabstop>associateButton</tabstop>
<tabstop>knownServersList</tabstop>
<tabstop>preferredServersList</tabstop>