diff options
| author | Jeremy Rimpo <jrim@rimpo.org> | 2019-07-22 01:02:21 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-07-22 01:02:21 -0500 |
| commit | 1f1b838c2b3fd3148d2c2a1a8fe6fcf9fb07383f (patch) | |
| tree | 33c20c0cc6e5f0029558c8e8383a0fdf68571689 /src/nxmaccessmanager.cpp | |
| parent | 613aa037cf8dc728eac474ee6cf6b790293d5114 (diff) | |
| parent | 2d78957c3d4c33de5813d7ee5b86dec8cb01478c (diff) | |
Merge pull request #796 from isanae/login-progress-dialog
Nexus login progress dialog and Nexus settings
Diffstat (limited to 'src/nxmaccessmanager.cpp')
| -rw-r--r-- | src/nxmaccessmanager.cpp | 721 |
1 files changed, 558 insertions, 163 deletions
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.")); - } -} |
