diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index a27d89e678..a31d117f59 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -47,6 +47,9 @@ Q_DECLARE_METATYPE(JSONCallbackParameters) const QString ACCOUNTS_GROUP = "accounts"; +const int POST_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; +const int PULL_SETTINGS_RETRY_INTERVAL = 1 * MSECS_PER_SECOND; + JSONCallbackParameters::JSONCallbackParameters(QObject* callbackReceiver, const QString& jsonCallbackMethod, const QString& errorCallbackMethod) : @@ -87,12 +90,20 @@ AccountManager::AccountManager(UserAgentGetter userAgentGetter) : qRegisterMetaType(); connect(this, &AccountManager::loginComplete, this, &AccountManager::uploadPublicKey); + connect(this, &AccountManager::loginComplete, this, &AccountManager::requestAccountSettings); + + _postSettingsTimer = new QTimer(this); + _postSettingsTimer->setInterval(POST_SETTINGS_INTERVAL); + connect(this, SIGNAL(loginComplete(QUrl)), _postSettingsTimer, SLOT(start())); + connect(this, &AccountManager::logoutComplete, _postSettingsTimer, &QTimer::stop); + connect(_postSettingsTimer, &QTimer::timeout, this, &AccountManager::postAccountSettings); + connect(qApp, &QCoreApplication::aboutToQuit, this, &AccountManager::postAccountSettings); } -const QString DOUBLE_SLASH_SUBSTITUTE = "slashslash"; const QString ACCOUNT_MANAGER_REQUESTED_SCOPE = "owner"; void AccountManager::logout() { + postAccountSettings(); // a logout means we want to delete the DataServerAccountInfo we currently have for this URL, in-memory and in file _accountInfo = DataServerAccountInfo(); @@ -104,6 +115,8 @@ void AccountManager::logout() { emit logoutComplete(); // the username has changed to blank emit usernameChanged(QString()); + + _settings.loggedOut(); } QString accountFileDir() { @@ -160,33 +173,7 @@ void AccountManager::setAuthURL(const QUrl& authURL) { qCDebug(networking) << "Found metaverse API account information for" << qPrintable(_authURL.toString()); } else { - // we didn't have a file - see if we can migrate old settings and store them in the new file - - // check if there are existing access tokens to load from settings - Settings settings; - settings.beginGroup(ACCOUNTS_GROUP); - - foreach(const QString& key, settings.allKeys()) { - // take a key copy to perform the double slash replacement - QString keyCopy(key); - QUrl keyURL(keyCopy.replace(DOUBLE_SLASH_SUBSTITUTE, "//")); - - if (keyURL == _authURL) { - // pull out the stored access token and store it in memory - _accountInfo = settings.value(key).value(); - - qCDebug(networking) << "Migrated an access token for" << qPrintable(keyURL.toString()) - << "from previous settings file"; - } - } - settings.endGroup(); - - if (_accountInfo.getAccessToken().token.isEmpty()) { - qCWarning(networking) << "Unable to load account file. No existing account settings will be loaded."; - } else { - // persist the migrated settings to file - persistAccountToFile(); - } + qCWarning(networking) << "Unable to load account file. No existing account settings will be loaded."; } if (_isAgent && !_accountInfo.getAccessToken().token.isEmpty() && !_accountInfo.hasProfile()) { @@ -199,6 +186,10 @@ void AccountManager::setAuthURL(const QUrl& authURL) { refreshAccessToken(); } + if (isLoggedIn()) { + emit loginComplete(_authURL); + } + // tell listeners that the auth endpoint has changed emit authEndpointChanged(); } @@ -804,6 +795,99 @@ void AccountManager::requestProfileError(QNetworkReply::NetworkError error) { qCDebug(networking) << "AccountManager requestProfileError - " << error; } +void AccountManager::requestAccountSettings() { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QUrl lockerURL = _authURL; + lockerURL.setPath("/api/v1/user/locker"); + + QNetworkRequest lockerRequest(lockerURL); + lockerRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + lockerRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); + lockerRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, _accountInfo.getAccessToken().authorizationHeaderValue()); + + QNetworkReply* lockerReply = networkAccessManager.get(lockerRequest); + connect(lockerReply, &QNetworkReply::finished, this, &AccountManager::requestAccountSettingsFinished); + connect(lockerReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestAccountSettingsError(QNetworkReply::NetworkError))); + + _settings.startedLoading(); +} + +void AccountManager::requestAccountSettingsFinished() { + QNetworkReply* lockerReply = reinterpret_cast(sender()); + + QJsonDocument jsonResponse = QJsonDocument::fromJson(lockerReply->readAll()); + const QJsonObject& rootObject = jsonResponse.object(); + + if (rootObject.contains("status") && rootObject["status"].toString() == "success") { + if (rootObject.contains("data") && rootObject["data"].isObject()) { + _settings.unpack(rootObject["data"].toObject()); + + emit accountSettingsLoaded(); + } else { + qCDebug(networking) << "Error in response for account settings: no data object"; + QTimer::singleShot(PULL_SETTINGS_RETRY_INTERVAL, this, &AccountManager::requestAccountSettings); + } + } else { + qCDebug(networking) << "Error in response for account settings" << lockerReply->errorString(); + QTimer::singleShot(PULL_SETTINGS_RETRY_INTERVAL, this, &AccountManager::requestAccountSettings); + } +} + +void AccountManager::requestAccountSettingsError(QNetworkReply::NetworkError error) { + qCWarning(networking) << "Account settings request encountered an error" << error; + QTimer::singleShot(PULL_SETTINGS_RETRY_INTERVAL, this, &AccountManager::requestAccountSettings); +} + +void AccountManager::postAccountSettings() { + if (_settings.lastChangeTimestamp() <= _lastSuccessfulSyncTimestamp && _lastSuccessfulSyncTimestamp != 0) { + // Nothing changed, skipping settings post + return; + } + if (!isLoggedIn()) { + qCWarning(networking) << "Can't post account settings: Not logged in"; + return; + } + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QUrl lockerURL = _authURL; + lockerURL.setPath("/api/v1/user/locker"); + + QNetworkRequest lockerRequest(lockerURL); + lockerRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + lockerRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); + lockerRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + lockerRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, _accountInfo.getAccessToken().authorizationHeaderValue()); + + _currentSyncTimestamp = _settings.lastChangeTimestamp(); + QJsonObject dataObj; + dataObj.insert("locker", _settings.pack()); + + auto postData = QJsonDocument(dataObj).toJson(QJsonDocument::Compact); + + QNetworkReply* lockerReply = networkAccessManager.put(lockerRequest, postData); + connect(lockerReply, &QNetworkReply::finished, this, &AccountManager::postAccountSettingsFinished); + connect(lockerReply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(postAccountSettingsError(QNetworkReply::NetworkError))); +} + +void AccountManager::postAccountSettingsFinished() { + QNetworkReply* lockerReply = reinterpret_cast(sender()); + + QJsonDocument jsonResponse = QJsonDocument::fromJson(lockerReply->readAll()); + const QJsonObject& rootObject = jsonResponse.object(); + + if (rootObject.contains("status") && rootObject["status"].toString() == "success") { + _lastSuccessfulSyncTimestamp = _currentSyncTimestamp; + } else { + qCDebug(networking) << "Error in response for account settings post" << lockerReply->errorString(); + } +} + +void AccountManager::postAccountSettingsError(QNetworkReply::NetworkError error) { + qCWarning(networking) << "Post encountered an error" << error; +} + void AccountManager::generateNewKeypair(bool isUserKeypair, const QUuid& domainID) { if (thread() != QThread::currentThread()) { diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index a88a5cc4e7..f29221d671 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -14,18 +14,19 @@ #include #include +#include #include #include #include +#include + +#include "AccountSettings.h" +#include "DataServerAccountInfo.h" #include "NetworkingConstants.h" #include "NetworkAccessManager.h" - -#include "DataServerAccountInfo.h" #include "SharedUtil.h" -#include - class JSONCallbackParameters { public: JSONCallbackParameters(QObject* callbackReceiver = nullptr, @@ -107,6 +108,8 @@ public: void setConfigFileURL(const QString& fileURL) { _configFileURL = fileURL; } void saveLoginStatus(bool isLoggedIn); + AccountSettings& getAccountSettings() { return _settings; } + public slots: void requestAccessToken(const QString& login, const QString& password); void requestAccessTokenWithSteam(QByteArray authSessionTicket); @@ -136,6 +139,7 @@ signals: void logoutComplete(); void newKeypair(); void limitedCommerceChanged(); + void accountSettingsLoaded(); private slots: void handleKeypairGenerationError(); @@ -145,6 +149,13 @@ private slots: void publicKeyUploadFailed(QNetworkReply* reply); void generateNewKeypair(bool isUserKeypair = true, const QUuid& domainID = QUuid()); + void requestAccountSettings(); + void requestAccountSettingsFinished(); + void requestAccountSettingsError(QNetworkReply::NetworkError error); + void postAccountSettings(); + void postAccountSettingsFinished(); + void postAccountSettingsError(QNetworkReply::NetworkError error); + private: AccountManager(AccountManager const& other) = delete; void operator=(AccountManager const& other) = delete; @@ -170,6 +181,11 @@ private: bool _limitedCommerce { false }; QString _configFileURL; + + AccountSettings _settings; + quint64 _currentSyncTimestamp { 0 }; + quint64 _lastSuccessfulSyncTimestamp { 0 }; + QTimer* _postSettingsTimer { nullptr }; }; #endif // hifi_AccountManager_h diff --git a/libraries/networking/src/AccountSettings.cpp b/libraries/networking/src/AccountSettings.cpp new file mode 100644 index 0000000000..6562826dd7 --- /dev/null +++ b/libraries/networking/src/AccountSettings.cpp @@ -0,0 +1,55 @@ +// +// AccountSettings.cpp +// libraries/networking/src +// +// Created by Clement Brisset on 9/12/19. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AccountSettings.h" + +#include +#include + +#include "NetworkLogging.h" +#include "SharedUtil.h" + +static QString HOME_LOCATION_KEY { "home_location" }; + +QJsonObject AccountSettings::pack() { + QJsonObject data; + + QReadLocker lock(&_settingsLock); + data.insert(HOME_LOCATION_KEY, _homeLocation); + + return data; +} + +void AccountSettings::unpack(QJsonObject data) { + QWriteLocker lock(&_settingsLock); + + _lastChangeTimestamp = usecTimestampNow(); + + auto it = data.find(HOME_LOCATION_KEY); + _homeLocationState = it != data.end() && it->isString() ? Loaded : NotPresent; + _homeLocation = _homeLocationState == Loaded ? it->toString() : ""; +} + +void AccountSettings::setHomeLocation(QString homeLocation) { + QWriteLocker lock(&_settingsLock); + if (homeLocation != _homeLocation) { + _lastChangeTimestamp = usecTimestampNow(); + } + _homeLocation = homeLocation; +} + +void AccountSettings::startedLoading() { + _homeLocationState = Loading; +} + +void AccountSettings::loggedOut() { + _homeLocationState = LoggedOut; +} diff --git a/libraries/networking/src/AccountSettings.h b/libraries/networking/src/AccountSettings.h new file mode 100644 index 0000000000..7bec5ce767 --- /dev/null +++ b/libraries/networking/src/AccountSettings.h @@ -0,0 +1,47 @@ +// +// AccountSettings.h +// libraries/networking/src +// +// Created by Clement Brisset on 9/12/19. +// Copyright 2019 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AccountSettings_h +#define hifi_AccountSettings_h + +#include +#include +#include + +class AccountSettings { +public: + enum State { + LoggedOut, + Loading, + Loaded, + NotPresent + }; + + void loggedOut(); + void startedLoading(); + quint64 lastChangeTimestamp() const { return _lastChangeTimestamp; } + + QJsonObject pack(); + void unpack(QJsonObject data); + + State homeLocationState() const { QReadLocker lock(&_settingsLock); return _homeLocationState; } + QString getHomeLocation() const { QReadLocker lock(&_settingsLock); return _homeLocation; } + void setHomeLocation(QString homeLocation); + +private: + mutable QReadWriteLock _settingsLock; + quint64 _lastChangeTimestamp { 0 }; + + State _homeLocationState { LoggedOut }; + QString _homeLocation; +}; + +#endif /* hifi_AccountSettings_h */