From 7a116bfdf2192ffe6e0ca246474af514b6d250a6 Mon Sep 17 00:00:00 2001 From: blue Date: Sat, 20 Jan 2024 18:17:21 -0300 Subject: [PATCH] big refactoring of API system --- API/CMakeLists.txt | 2 +- API/api.cpp | 527 +++++--------------------- API/api.h | 78 ++-- API/helpers.h | 2 + API/requests/CMakeLists.txt | 26 ++ API/requests/addasset.cpp | 10 + API/requests/addasset.h | 19 + API/requests/listassets.cpp | 18 + API/requests/listassets.h | 23 ++ API/requests/login.cpp | 34 ++ API/requests/login.h | 23 ++ API/requests/poll.cpp | 46 +++ API/requests/poll.h | 25 ++ API/requests/post.cpp | 17 + API/requests/post.h | 25 ++ API/requests/register.cpp | 29 ++ API/requests/register.h | 21 + API/requests/request.cpp | 134 +++++++ API/requests/request.h | 69 ++++ API/requests/test.cpp | 27 ++ API/requests/test.h | 23 ++ CMakeLists.txt | 2 + {API/models => models}/CMakeLists.txt | 2 + {API/models => models}/assets.cpp | 17 +- {API/models => models}/assets.h | 6 +- models/magpie.cpp | 200 ++++++++++ models/magpie.h | 81 ++++ qml/Application/Assets.qml | 6 +- qml/Components/AssetLine.qml | 41 +- qml/Forms/AddAsset.qml | 2 +- qml/Forms/Login.qml | 2 +- qml/Forms/Register.qml | 2 +- qml/ServerPick.qml | 2 +- qml/Welcome.qml | 16 +- qml/main.qml | 10 +- root.cpp | 21 +- root.h | 6 +- 37 files changed, 1060 insertions(+), 534 deletions(-) create mode 100644 API/requests/CMakeLists.txt create mode 100644 API/requests/addasset.cpp create mode 100644 API/requests/addasset.h create mode 100644 API/requests/listassets.cpp create mode 100644 API/requests/listassets.h create mode 100644 API/requests/login.cpp create mode 100644 API/requests/login.h create mode 100644 API/requests/poll.cpp create mode 100644 API/requests/poll.h create mode 100644 API/requests/post.cpp create mode 100644 API/requests/post.h create mode 100644 API/requests/register.cpp create mode 100644 API/requests/register.h create mode 100644 API/requests/request.cpp create mode 100644 API/requests/request.h create mode 100644 API/requests/test.cpp create mode 100644 API/requests/test.h rename {API/models => models}/CMakeLists.txt (87%) rename {API/models => models}/assets.cpp (90%) rename {API/models => models}/assets.h (95%) create mode 100644 models/magpie.cpp create mode 100644 models/magpie.h diff --git a/API/CMakeLists.txt b/API/CMakeLists.txt index c2f1ee4..1ece631 100644 --- a/API/CMakeLists.txt +++ b/API/CMakeLists.txt @@ -17,4 +17,4 @@ set(SOURCES target_sources(magpie PRIVATE ${SOURCES}) -add_subdirectory(models) +add_subdirectory(requests) diff --git a/API/api.cpp b/API/api.cpp index 296270c..1295971 100644 --- a/API/api.cpp +++ b/API/api.cpp @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: 2023 Yury Gubich -// SPDX-License-Identifier: GPL-3.0-or-later +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later #include "api.h" #include @@ -7,466 +7,128 @@ #include #include -#include "codes.h" -#include "finalaction.h" -#include "helpers.h" +#include "requests/test.h" +#include "requests/register.h" +#include "requests/login.h" +#include "requests/poll.h" +#include "requests/listassets.h" +#include "requests/addasset.h" -constexpr const char* json = "application/json"; -constexpr const char* urlEncoded = "application/x-www-form-urlencoded"; - -const std::map testStructure({ - {"type", QMetaType::QString}, - {"version", QMetaType::QString}, -}); - -const std::map resultStructure({ - {"result", QMetaType::LongLong}, -}); - -const std::map tokensStructure({ - {"accessToken", QMetaType::QString}, - {"renewToken", QMetaType::QString} -}); - -struct NetworkReplyDeleter { - void operator () (QNetworkReply* reply) { - reply->deleteLater(); - } -}; - -API::API(const QUrl& address, QObject* parent): +API::API (Models::Magpie& magpie, QObject* parent): QObject(parent), - address(address), + idCounter(0), + magpie(magpie), network(), - state(NoServer), - accessToken(), - renewToken(), - firstPoll(), pollReply(), - assets() -{ - firstPoll.setSingleShot(true); - firstPoll.setInterval(2000); - connect(&firstPoll, &QTimer::timeout, this, &API::onFirstPollSuccess); - connect(&assets, &Models::Assets::requestAssets, this, &API::requestAssets); + requests() +{} + +void API::cancelRequest (RequestId id) { + requests.erase(id); } -QUrl API::getAddress() const { - return address; +void API::onRequestDone (RequestId id) { + requests.erase(id); } -API::State API::getState() const { - return state; -} - -void API::setTokens(const QString access, const QString &renew) { - accessToken = access; - renewToken = renew; - setState(Authenticating); - startPolling(); -} - -void API::startPolling() { - qDebug() << "Starting polling..."; - if (state != Authenticating) - throw std::runtime_error("Can not start polling in this state: " + std::to_string(state)); - - sendPoll(); - firstPoll.start(); -} - -void API::setAddress(const QUrl& path) { - if (address == path) - return; - - if (state == Authenticated) { - //do something - } - - address = path; - emit addressChanged(address); - setState(address.isEmpty() ? NoServer : NotAuthenticated); -} - -std::optional API::readResult(QNetworkReply* reply) { - QVariant contentType = reply->header(QNetworkRequest::ContentTypeHeader); - if (! - contentType.isValid() || - contentType.userType() != QMetaType::QString || - contentType.toString() != json - ) - return std::nullopt; - - QByteArray data = reply->readAll(); - QJsonDocument document = QJsonDocument::fromJson(data); - if (!document.isObject()) - return std::nullopt; - - QJsonObject object = document.object(); - return object.toVariantMap(); -} - -bool API::validateResponse(const std::optional& data, const std::map& structure) { - if (!data.has_value()) - return false; - - for (const std::pair& pair : structure) { - QVariantMap::ConstIterator itr = data->find(pair.first); - if (itr == data->end()) - return false; - - if (itr->userType() != pair.second) - return false; - } - - return true; -} - -void API::setState(State newState) { - if (newState == state) - return; - - state = newState; - emit stateChanged(state); -} - -QUrl API::createUrl(const QString &path) const { - QString startingPath = address.path(); - QUrl url = address; - url.setPath(startingPath + path); - return url; -} - -void API::test(const QString& path, const QJSValue& finished) { +API::RequestId API::test (const QString& path, const QJSValue& finished) { qDebug() << "Testing" << path; - if (state == Offline) - return callCallback(finished, "Need to be online to test"); + if (magpie.getState() == Models::Magpie::Offline) + callCallback(finished, "Need to be online to test"), 0; - QUrl address(path); - QNetworkRequest request(path + "/info"); - request.setHeader(QNetworkRequest::ContentTypeHeader, json); - - QNetworkReply* reply = network.get(request); - connect(reply, &QNetworkReply::finished, - std::bind(&API::onTestFinished, this, reply, address, finished) - ); -} - -void API::onTestFinished(QNetworkReply* reply, const QUrl& addr, const QJSValue& finished) { - std::unique_ptr rpl(reply); - QNetworkReply::NetworkError error = reply->error(); - if (error != QNetworkReply::NoError) - return callCallback(finished, reply->errorString()); - - std::optional data = readResult(reply); - if (!validateResponse(data, testStructure)) - return callCallback(finished, "Malformed response"); - - QString type = data->value("type").toString(); - if (type != "pica") - return callCallback(finished, "server of this type (" + type + ") is not supported"); - - QString version = data->value("version").toString(); - if (version != "0.0.1") - return callCallback(finished, "server of this version (" + version + ") is not supported"); - - callCallback(finished, QString(), {QJSValue(true)}); - address = ""; //to provoke singal change even if it's the same server - setAddress(addr); -} - -void API::sendRegister(const QString& login, const QString& password, const QJSValue& finished) { - qDebug() << "Registering..."; - if (state != NotAuthenticated) - return callCallback(finished, "Can not register in current state"); - - QUrlQuery params({ - {"login", login}, - {"password", password} - }); - - QNetworkRequest request(createUrl("/register")); - request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded); - - QNetworkReply* reply = network.post(request, params.toString(QUrl::FullyEncoded).toUtf8()); - connect(reply, &QNetworkReply::finished, - std::bind(&API::onRegisterFinished, this, reply, finished) - ); -} - -void API::onRegisterFinished(QNetworkReply* reply, const QJSValue& finished) const { - std::unique_ptr rpl(reply); - QNetworkReply::NetworkError error = reply->error(); - std::optional data = readResult(reply); - std::optional code; - QString detail; - bool success = false; - if (validateResponse(data, resultStructure)) { - code = Codes::convertRegister(data->value("result").toInt()); - success = code.value() == Codes::Register::success; - if (!success) - detail = Codes::description(code.value()); - } - - if (error != QNetworkReply::NoError) - return callCallback(finished, reply->errorString() + (success ? "" : ": " + detail)); - - if (!code) - return callCallback(finished, "Malformed result"); - - callCallback(finished, detail, {QJSValue(success)}); -} - -void API::sendLogin(const QString& login, const QString& password, const QJSValue& finished) { - qDebug() << "Logging in..."; - if (state != NotAuthenticated) - return callCallback(finished, "Can not register in current state"); - - QUrlQuery params({ - {"login", login}, - {"password", password} - }); - - QNetworkRequest request(createUrl("/login")); - request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded); - setState(Authenticating); - QNetworkReply* reply = network.post(request, params.toString(QUrl::FullyEncoded).toUtf8()); - connect(reply, &QNetworkReply::finished, - std::bind(&API::onLoginFinished, this, reply, finished) - ); -} - -void API::onLoginFinished(QNetworkReply* reply, const QJSValue& finished) { - State state = NotAuthenticated; - FinalAction action([this, &state]() { //this should be executed on leaving the function in any way - setState(state); //setting the state as a result of this object destruction - }); //this way any error will result in NotAuthenticated, and on success it should be Authenticated - - std::unique_ptr rpl(reply); - QNetworkReply::NetworkError error = reply->error(); - std::optional data = readResult(reply); - std::optional code; - QString detail; - bool success = false; - if (validateResponse(data, resultStructure)) { - code = Codes::convertLogin(data->value("result").toInt()); - success = code.value() == Codes::Login::success; - if (!success) - detail = Codes::description(code.value()); - } - - if (error != QNetworkReply::NoError) - return callCallback(finished, reply->errorString() + (success ? "" : ": " + detail)); - - if (!code) - return callCallback(finished, "Malformed result"); - - if (!validateResponse(data, tokensStructure)) - return callCallback(finished, "Malformed result: missing tokens"); - - - callCallback(finished, detail, {QJSValue(success)}); - - state = Authenticating; - accessToken = data->value("accessToken").toString(); - renewToken = data->value("renewToken").toString(); - emit storeTokens(accessToken, renewToken); - startPolling(); -} - -void API::addAsset(const QString& title, const QString& icon, const QJSValue &finished) { - qDebug() << "Adding asset..."; - if (state != Authenticated) - return callCallback(finished, "Can not add assets in current state"); - - QUrlQuery params({ - {"title", title}, - {"icon", icon}, - {"currency", "1"} - }); - - QNetworkRequest request(createUrl("/addAsset")); - request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded); - request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8()); - QNetworkReply* reply = network.post(request, params.toString(QUrl::FullyEncoded).toUtf8()); - connect(reply, &QNetworkReply::finished, - this, std::bind(&API::onAssetAdded, this, reply, finished), - Qt::QueuedConnection - ); -} - -void API::onAssetAdded(QNetworkReply *reply, const QJSValue &finished) { - std::unique_ptr rpl(reply); - QNetworkReply::NetworkError error = reply->error(); - - qDebug() << error; - if (error != QNetworkReply::NoError) - return callCallback(finished, reply->errorString()); - - callCallback(finished, QString(), {QJSValue(true)}); -} - -void API::sendPoll(bool clear) { - if (accessToken.isEmpty()) - throw std::runtime_error("Can not start polling: access token is empty"); - - QUrl url = createUrl("/poll"); - if (clear) - url.setQuery(QUrlQuery({{"clearCache", "all"}})); - - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, json); - request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8()); - request.setTransferTimeout(30000); - - pollReply = std::unique_ptr(network.get(request)); + std::unique_ptr test = std::make_unique(path); connect( - pollReply.get(), &QNetworkReply::finished, - this, &API::onPollFinished, Qt::QueuedConnection + test.get(), + &Request::Test::success, + this, + [this, &path, &finished] (const QVariantMap& data) { + callCallback(finished, QString(), {QJSValue(true)}); + magpie.forceAddress(path); + } ); + connect(test.get(), &Request::Test::error, std::bind(&API::callCallback, this, finished, std::placeholders::_1, QJSValueList(false))); + return registerAndSend(std::move(test)); } -void API::onPollFinished() { - firstPoll.stop(); - QNetworkReply::NetworkError error = pollReply->error(); - std::optional data = readResult(pollReply.get()); - Codes::Poll code = Codes::Poll::unknownError; - if (validateResponse(data, resultStructure)) - code = Codes::convertPoll(data->value("result").toInt()); - else - qDebug() << ""; +API::RequestId API::sendRegister (const QString& login, const QString& password, const QJSValue& finished) { + qDebug() << "Registering..."; + if (magpie.getState() != Models::Magpie::NotAuthenticated) + return callCallback(finished, "Can not register in current state"), 0; - QString detail = Codes::description(code); + std::unique_ptr reg = std::make_unique(login, password, magpie.getAddress()); + connect(reg.get(), &Request::Register::success, std::bind(&API::callCallback, this, finished, QString(), QJSValueList{QJSValue(true)})); + connect(reg.get(), &Request::Register::error, std::bind(&API::callCallback, this, finished, std::placeholders::_1, QJSValueList{QJSValue(false)})); + return registerAndSend(std::move(reg)); +} - if (error != QNetworkReply::NoError) - qDebug() << pollReply->errorString() + ": " + detail; - else - qDebug() << "Poll finished: " + detail; +API::RequestId API::sendLogin (const QString& login, const QString& password, const QJSValue& finished) { + qDebug() << "Logging in..."; + if (magpie.getState() != Models::Magpie::NotAuthenticated) + return callCallback(finished, "Can not register in current state"), 0; - bool clear = false; - switch (code) { - case Codes::Poll::success: { - QVariantMap::ConstIterator itr = data->constFind("data"); - if (itr == data->constEnd()) - qDebug("received a poll that claimed to have some updates, but the \"data\" field is abscent"); - - const QVariant& vdata = itr.value(); - if (vdata.canConvert()) - clear = handleChanges(qast(itr.value())); - else - qDebug("received a poll that claimed to have some updates, but the \"data\" field is not an object"); + std::unique_ptr log = std::make_unique(login, password, magpie.getAddress()); + connect( + log.get(), + &Request::Login::success, + this, + [this, &finished] (const QVariantMap& data) { + callCallback(finished, QString(), {QJSValue(true)}); + magpie.setTokens(data.value("accessToken").toString(), data.value("renewToken").toString()); } - //todo handle the result - case Codes::Poll::timeout: - setState(Authenticated); - return sendPoll(clear); - case Codes::Poll::tokenProblem: - case Codes::Poll::replace: - case Codes::Poll::unknownError: //todo this one doesn't actually mean that we can't work for now, the network may be temporarily down or something - pollReply.reset(); - return setState(NotAuthenticated); - } -} - -bool API::handleChanges(const QVariantMap& changes) { - QVariantMap::ConstIterator itr = changes.constFind("system"); - if (itr != changes.constEnd() && itr.value().canConvert()) { - const QVariantMap& sys = qast(itr.value()); - QVariantMap::ConstIterator invItr = sys.constFind("invalidate"); - if (invItr != sys.constEnd()) { - const QVariant& vinv = invItr.value(); - if (vinv.canConvert() && vinv.toBool()) - resetAllModels(); + ); + connect( + log.get(), + &Request::Login::error, + this, + [this, &finished] (const QString& error) { + callCallback(finished, error, {QJSValue(false)}); + magpie.setState(Models::Magpie::NotAuthenticated); } - } + ); - itr = changes.constFind("assets"); - if (itr != changes.constEnd() && itr.value().canConvert()) { - const QVariantMap& assets = qast(itr.value()); - QVariantMap::ConstIterator aItr = assets.constFind("invalidate"); - if (aItr != assets.constEnd()) { - const QVariant& vinv = aItr.value(); - if (vinv.canConvert() && vinv.toBool()) - API::assets.clear(); - } - - aItr = assets.constFind("added"); - if (aItr != assets.constEnd() && aItr.value().canConvert()) { - std::deque added; - if (!Models::Assets::deserialize(qast(aItr.value()), added)) - qDebug() << "Error deserializng added assets"; - else - API::assets.addAssets(added); - } - } - - return true; + magpie.setState(Models::Magpie::Authenticating); + return registerAndSend(std::move(log)); } -void API::resetAllModels() { - assets.clear(); +API::RequestId API::addAsset (const QString& title, const QString& icon, const QJSValue& finished) { + qDebug() << "Adding asset..."; + if (magpie.getState() != Models::Magpie::Authenticated) + return callCallback(finished, "Can not add assets in current state"), 0; + + auto add = std::make_unique(title, icon, QColor::fromString("black"), 1, magpie.getAddress()); + add->setAuthorizationToken(magpie.getAccessToken()); + connect(add.get(), &Request::AddAsset::success, std::bind(&API::callCallback, this, finished, QString(), QJSValueList{QJSValue(true)})); + connect(add.get(), &Request::AddAsset::error, std::bind(&API::callCallback, this, finished, std::placeholders::_1, QJSValueList{QJSValue(false)})); + return registerAndSend(std::move(add)); } -void API::onFirstPollSuccess() { - setState(Authenticated); -} - -void API::requestAssets() { - if (state != Authenticated) { - qDebug() << "An attempt to request assets on unauthenticated state"; - assets.receivedAssets({}); - return; - } +API::RequestId API::requestAssets (const SuccessListHandler& success, const ErrorHandler& error) { qDebug() << "Requesting assets..."; - QUrl url = createUrl("/listAssets"); - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, json); - request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8()); - - QNetworkReply* reply = network.get(request); - connect( - reply, &QNetworkReply::finished, - this, std::bind(&API::responseAssets, this, reply) - ); + auto list = std::make_unique(magpie.getAddress()); + list->setAuthorizationToken(magpie.getAccessToken()); + connect(list.get(), &Request::ListAssets::success, success); + connect(list.get(), &Request::ListAssets::error, error); + return registerAndSend(std::move(list)); } -void API::responseAssets(QNetworkReply *reply) { - std::deque result; - FinalAction action([this, &result]() { - assets.receivedAssets(result); - }); - - std::unique_ptr rpl(reply); - QNetworkReply::NetworkError error = reply->error(); - - if (error != QNetworkReply::NoError) { - qDebug() << "Error receiving assets:" << reply->errorString(); - return; - } - - std::optional data = readResult(reply); - if (!data) { - qDebug() << "Error receiving assets: bad data"; - return; - } - - QVariantMap::ConstIterator itr = data->find("assets"); - if (itr == data->constEnd() || !itr->canConvert()) { - qDebug() << "Error receiving assets: assets are missing or not in an array"; - return; - } - - if (!Models::Assets::deserialize(qast(itr.value()), result)) { - qDebug() << "Error deserializng assets"; - result.clear(); - } - - qDebug() << "Assets successfully received"; - //final action goes automatically +API::RequestId API::poll (const SuccessMapHandler& success, const ErrorHandler& error, bool clear) { + auto poll = std::make_unique(magpie.getAddress(), clear); + poll->setAuthorizationToken(magpie.getAccessToken()); + connect(poll.get(), &Request::Poll::success, success); + connect(poll.get(), &Request::Poll::error, error); + return registerAndSend(std::move(poll)); } -void API::callCallback(const QJSValue& callback, const QString& error, const QJSValueList& arguments) const { +API::RequestId API::registerAndSend (std::unique_ptr request) { + Request::Request* req = request.get(); + requests.emplace(++idCounter, std::move(request)); + connect(req, &Request::Request::done, std::bind(&API::onRequestDone, this, idCounter)); + req->send(network); + return idCounter; +} + +void API::callCallback (const QJSValue& callback, const QString& error, const QJSValueList& arguments) const { if (callback.isCallable()) { if (error.isEmpty()) callback.call(QJSValueList({QJSValue(QJSValue::NullValue)}) + arguments); @@ -474,4 +136,3 @@ void API::callCallback(const QJSValue& callback, const QString& error, const QJS callback.call({QJSValue(error)}); } } - diff --git a/API/api.h b/API/api.h index 0e58143..c0ab576 100644 --- a/API/api.h +++ b/API/api.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -13,80 +14,45 @@ #include #include #include -#include -#include "models/assets.h" +#include "models/magpie.h" +#include "requests/request.h" -class Root; class API : public QObject { - friend class Root; Q_OBJECT -public: - enum State { - Offline, - NoServer, - NotAuthenticated, - Authenticating, - Authenticated - }; - Q_ENUM(State) - -private: - Q_PROPERTY(QUrl address READ getAddress NOTIFY addressChanged) - Q_PROPERTY(State state READ getState NOTIFY stateChanged) public: - explicit API(const QUrl& path = QString(), QObject* parent = nullptr); + using SuccessMapHandler = std::function; + using SuccessListHandler = std::function; + using ErrorHandler = std::function&)>; + using RequestId = unsigned int; - QUrl getAddress() const; - State getState() const; - void setTokens(const QString access, const QString& renew); - void startPolling(); + explicit API(Models::Magpie& magpie, QObject* parent = nullptr); -signals: - void addressChanged(const QUrl& path); - void stateChanged(State state); - void storeTokens(const QString& access, const QString& renew); + RequestId requestAssets(const SuccessListHandler& success, const ErrorHandler& error); + RequestId poll(const SuccessMapHandler& success, const ErrorHandler& error, bool clear = false); + + static const RequestId none = 0; public slots: - void test(const QString& path, const QJSValue& finished = QJSValue()); - void sendRegister(const QString& login, const QString& password, const QJSValue& finished = QJSValue()); - void sendLogin(const QString& login, const QString& password, const QJSValue& finished = QJSValue()); - void addAsset(const QString& title, const QString& icon, const QJSValue& finished = QJSValue()); + void cancelRequest(RequestId id); + RequestId test(const QString& path, const QJSValue& finished = QJSValue()); + RequestId sendRegister(const QString& login, const QString& password, const QJSValue& finished = QJSValue()); + RequestId sendLogin(const QString& login, const QString& password, const QJSValue& finished = QJSValue()); + RequestId addAsset(const QString& title, const QString& icon, const QJSValue& finished = QJSValue()); private slots: - void onTestFinished(QNetworkReply* reply, const QUrl& addr, const QJSValue& finished); - void onRegisterFinished(QNetworkReply* reply, const QJSValue& finished) const; - void onLoginFinished(QNetworkReply* reply, const QJSValue& finished); - void onAssetAdded(QNetworkReply* reply, const QJSValue& finished); - void onPollFinished(); - - void onFirstPollSuccess(); - - void requestAssets(); - void responseAssets(QNetworkReply* reply); + void onRequestDone(RequestId id); private: void callCallback(const QJSValue& callback, const QString& error = QString(), const QJSValueList& arguments = QJSValueList()) const; - void setAddress(const QUrl& path); - static std::optional readResult(QNetworkReply* reply); - static bool validateResponse(const std::optional& data, const std::map& structure); - void setState(State newState); - QUrl createUrl(const QString& path) const; - void sendPoll(bool clear = false); - bool handleChanges(const QVariantMap& changes); - void resetAllModels(); + RequestId registerAndSend(std::unique_ptr request); private: - QUrl address; + RequestId idCounter; + Models::Magpie& magpie; QNetworkAccessManager network; - State state; - QString accessToken; - QString renewToken; - QTimer firstPoll; + std::map> requests; std::unique_ptr pollReply; - -public: - Models::Assets assets; }; diff --git a/API/helpers.h b/API/helpers.h index d010b96..a496e99 100644 --- a/API/helpers.h +++ b/API/helpers.h @@ -6,6 +6,8 @@ #include #include +#define UNUSED(X) (void)(X) + template const T& qast(const QVariant& variant) { if (variant.userType() == qMetaTypeId()) diff --git a/API/requests/CMakeLists.txt b/API/requests/CMakeLists.txt new file mode 100644 index 0000000..a72d940 --- /dev/null +++ b/API/requests/CMakeLists.txt @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2023 Yury Gubich +# SPDX-License-Identifier: GPL-3.0-or-later + +set(HEADERS + request.h + test.h + post.h + register.h + login.h + poll.h + listassets.h + addasset.h +) + +set(SOURCES + request.cpp + test.cpp + post.cpp + register.cpp + login.cpp + poll.cpp + listassets.cpp + addasset.cpp +) + +target_sources(magpie PRIVATE ${SOURCES}) diff --git a/API/requests/addasset.cpp b/API/requests/addasset.cpp new file mode 100644 index 0000000..24170f3 --- /dev/null +++ b/API/requests/addasset.cpp @@ -0,0 +1,10 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "addasset.h" + +Request::AddAsset::AddAsset (const QString& title, const QString& icon, const QColor& color, unsigned int currency, const QUrl& baseUrl): + Post(createUrl(baseUrl, "/addAsset"), {{"title", title}, {"icon", icon}, {"currency", std::to_string(currency).c_str()}, {"color", "0"}}) +{ + emptyResult = true; +} diff --git a/API/requests/addasset.h b/API/requests/addasset.h new file mode 100644 index 0000000..8366327 --- /dev/null +++ b/API/requests/addasset.h @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "post.h" + +namespace Request { + +class AddAsset : public Post { + Q_OBJECT + +public: + AddAsset (const QString& title, const QString& icon, const QColor& color, unsigned int currency, const QUrl& baseUrl); +}; + +} diff --git a/API/requests/listassets.cpp b/API/requests/listassets.cpp new file mode 100644 index 0000000..45dfa33 --- /dev/null +++ b/API/requests/listassets.cpp @@ -0,0 +1,18 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "listassets.h" + +#include "API/helpers.h" + +Request::ListAssets::ListAssets (const QUrl& baseUrl): + Request(createUrl(baseUrl, "/listAssets")) {} + +void Request::ListAssets::onSuccess (const QVariantMap& data) { + QVariantMap::ConstIterator itr = data.find("assets"); + if (itr == data.constEnd() || !itr->canConvert()) + return Request::onError("Error receiving assets: assets are missing or not in an array", std::nullopt); + + emit success(qast(itr.value())); + emit done(); +} diff --git a/API/requests/listassets.h b/API/requests/listassets.h new file mode 100644 index 0000000..800d9a4 --- /dev/null +++ b/API/requests/listassets.h @@ -0,0 +1,23 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "request.h" + +namespace Request { + +class ListAssets : public Request { + Q_OBJECT + +public: + ListAssets (const QUrl& baseUrl); + +signals: + void success(const QVariantList& assets); + +protected: + void onSuccess (const QVariantMap& data) override; +}; + +} diff --git a/API/requests/login.cpp b/API/requests/login.cpp new file mode 100644 index 0000000..599725f --- /dev/null +++ b/API/requests/login.cpp @@ -0,0 +1,34 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "login.h" + +#include "API/codes.h" + +const std::map Request::Login::tokensStructure({ + {"accessToken", QMetaType::QString}, {"renewToken", QMetaType::QString} +}); + +Request::Login::Login (const QString& login, const QString& password, const QUrl& baseUrl): + Post(createUrl(baseUrl, "/login"), {{"login", login}, {"password", password}}) {} + +void Request::Login::onSuccess (const QVariantMap& data) { + if (!validateResponse(data, resultStructure)) + return Request::onError("Malformed response", std::nullopt); + + if (!validateResponse(data, tokensStructure)) + return Request::onError("Malformed result: missing tokens", std::nullopt); + + Codes::Login code = Codes::convertLogin(data.value("result").toInt()); + if (code != Codes::Login::success) + return Request::onError("Failed to login: " + Codes::description(code), data); + + Request::onSuccess(data); +} + +void Request::Login::onError (const QString& err, const std::optional& data) { + if (validateResponse(data, resultStructure)) + Request::onError(err + ": " + Codes::description(Codes::convertLogin(data->value("result").toInt())), data); + else + Request::onError(err, data); +} diff --git a/API/requests/login.h b/API/requests/login.h new file mode 100644 index 0000000..df84155 --- /dev/null +++ b/API/requests/login.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "post.h" + +namespace Request { + +class Login : public Post { + Q_OBJECT + +public: + Login (const QString& login, const QString& password, const QUrl& baseUrl); + +protected: + void onSuccess (const QVariantMap& data) override; + void onError (const QString& error, const std::optional& data) override; + + static const std::map tokensStructure; +}; + +} diff --git a/API/requests/poll.cpp b/API/requests/poll.cpp new file mode 100644 index 0000000..cc2b5f6 --- /dev/null +++ b/API/requests/poll.cpp @@ -0,0 +1,46 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "poll.h" + +#include + +#include "API/codes.h" + +const std::map Request::Poll::updatesStructure({ + {"data", QMetaType::QVariantMap}, +}); + +Request::Poll::Poll (const QUrl& baseUrl, bool clear): + Request(createUrl(baseUrl, "/poll", clear)) +{ + request.setTransferTimeout(30000); +} + +void Request::Poll::onSuccess (const QVariantMap& data) { + if (!validateResponse(data, resultStructure)) + return Request::onError("Malformed response", std::nullopt); + + Codes::Poll code = Codes::convertPoll(data.value("result").toInt()); + if (code == Codes::Poll::success) + if (!validateResponse(data, updatesStructure)) + return onError("Malformed response: received a poll that claimed to have some updates, but the \"data\" field is abscent", data); + + Request::onSuccess(data); +} + +void Request::Poll::onError (const QString& err, const std::optional& data) { + if (validateResponse(data, resultStructure)) + Request::onError(err + ": " + Codes::description(Codes::convertPoll(data->value("result").toInt())), data); + else + Request::onError(err, data); +} + +QUrl Request::Poll::createUrl (QUrl base, const QString& path, bool clear) { + QString startingPath = base.path(); + base.setPath(startingPath + path); + if (clear) + base.setQuery(QUrlQuery({{"clearCache", "all"}})); + + return base; +} diff --git a/API/requests/poll.h b/API/requests/poll.h new file mode 100644 index 0000000..3d56ec8 --- /dev/null +++ b/API/requests/poll.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "request.h" + +namespace Request { + +class Poll : public Request { + Q_OBJECT + +public: + Poll (const QUrl& baseUrl, bool clear = false); + +protected: + void onSuccess (const QVariantMap& data) override; + void onError (const QString& error, const std::optional& data) override; + + static QUrl createUrl(QUrl base, const QString& path, bool clear); + + static const std::map updatesStructure; +}; + +} diff --git a/API/requests/post.cpp b/API/requests/post.cpp new file mode 100644 index 0000000..9e307a9 --- /dev/null +++ b/API/requests/post.cpp @@ -0,0 +1,17 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "post.h" + +Request::Post::Post (const QUrl& url, const QUrlQuery& form): + Request(url), + form(form) +{} + +void Request::Post::aquireRequest (QNetworkAccessManager& manager) { + reply = std::unique_ptr(manager.post(request, form.toString(QUrl::FullyEncoded).toUtf8())); +} + +void Request::Post::initializeRequest () { + request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded); +} diff --git a/API/requests/post.h b/API/requests/post.h new file mode 100644 index 0000000..15f135e --- /dev/null +++ b/API/requests/post.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "request.h" + +#include + +namespace Request { + +class Post : public Request { + Q_OBJECT +public: + Post(const QUrl& url, const QUrlQuery& form); + +protected: + void aquireRequest (QNetworkAccessManager &manager) override; + void initializeRequest () override; + +protected: + QUrlQuery form; +}; + +} diff --git a/API/requests/register.cpp b/API/requests/register.cpp new file mode 100644 index 0000000..2a9a1c6 --- /dev/null +++ b/API/requests/register.cpp @@ -0,0 +1,29 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "register.h" + +#include + +#include "API/codes.h" + +Request::Register::Register (const QString& login, const QString& password, const QUrl& baseUrl): + Post(createUrl(baseUrl, "/register"), {{"login", login}, {"password", password}}) {} + +void Request::Register::onSuccess (const QVariantMap& data) { + if (!validateResponse(data, resultStructure)) + return Request::onError("Malformed response", std::nullopt); + + Codes::Register code = Codes::convertRegister(data.value("result").toInt()); + if (code != Codes::Register::success) + return Request::onError("Failed to register: " + Codes::description(code), data); + + Request::onSuccess(data); +} + +void Request::Register::onError (const QString& err, const std::optional& data) { + if (validateResponse(data, resultStructure)) + Request::onError(err + ": " + Codes::description(Codes::convertRegister(data->value("result").toInt())), data); + else + Request::onError(err, data); +} diff --git a/API/requests/register.h b/API/requests/register.h new file mode 100644 index 0000000..c02065a --- /dev/null +++ b/API/requests/register.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "post.h" + +namespace Request { + +class Register : public Post { + Q_OBJECT + +public: + Register (const QString& login, const QString& password, const QUrl& baseUrl); + +protected: + void onSuccess (const QVariantMap& data) override; + void onError (const QString& error, const std::optional& data) override; +}; + +} diff --git a/API/requests/request.cpp b/API/requests/request.cpp new file mode 100644 index 0000000..adace2b --- /dev/null +++ b/API/requests/request.cpp @@ -0,0 +1,134 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "request.h" + +#include +#include + +const std::map Request::Request::resultStructure({ + {"result", QMetaType::LongLong}, +}); + +Request::Request::Request (const QUrl& url): + state(State::initial), + request(url), + reply(), + emptyResult(false) +{} + +Request::Request::~Request () { + cancel(); +} + +void Request::Request::send (QNetworkAccessManager& manager) { + if (state != State::initial) + throw new std::runtime_error("An attempt to send a request in a wrong state"); + + initializeRequest(); + aquireRequest(manager); + connect(reply.get(), &QNetworkReply::finished, this, &Request::onFinished, Qt::QueuedConnection); + state = State::sent; +} + +void Request::Request::cancel () { + if (stateTerminal()) + return; + + state = State::cancelled; + if (reply) + reply->abort(); +} + +void Request::Request::setAuthorizationToken (const QString& token) { + request.setRawHeader("Authorization", "Bearer " + token.toUtf8()); +} + +void Request::Request::aquireRequest (QNetworkAccessManager& manager) { + reply = std::unique_ptr(manager.get(request)); +} + +void Request::Request::initializeRequest () { + request.setHeader(QNetworkRequest::ContentTypeHeader, json); +} + +std::optional Request::Request::validate () { + QNetworkReply::NetworkError error = reply->error(); + + if (error != QNetworkReply::NoError) + return reply->errorString(); + else + return std::nullopt; +} + +std::optional Request::Request::readResult () { + QVariant contentType = reply->header(QNetworkRequest::ContentTypeHeader); + if (!contentType.isValid() || !contentType.canConvert() || contentType.toString() != json) + return std::nullopt; + + QByteArray data = reply->readAll(); + QJsonDocument document = QJsonDocument::fromJson(data); + if (!document.isObject()) + return std::nullopt; + + QJsonObject object = document.object(); + return object.toVariantMap(); +} + +void Request::Request::onSuccess (const QVariantMap& data) { + emit success(data); + emit done(); +} + +void Request::Request::onError (const QString& err, const std::optional& data) { + emit error(err, data); + emit done(); +} + +bool Request::Request::validateResponse (const std::optional& data, const std::map& structure) { + if (!data.has_value()) + return false; + + for (const std::pair& pair : structure) { + QVariantMap::ConstIterator itr = data->find(pair.first); + if (itr == data->end()) + return false; + + if (itr->userType() != pair.second) + return false; + } + + return true; +} + +QUrl Request::Request::createUrl (QUrl base, const QString& path) { + QString startingPath = base.path(); + base.setPath(startingPath + path); + return base; +} + +bool Request::Request::stateTerminal () const { + return state == State::responded || state == State::cancelled; +} + +void Request::Request::onFinished () { + if (state != State::sent) + return; + + state = State::responded; + std::optional err = validate(); + std::optional data; + if (!emptyResult) + data = readResult(); + + if (err) + return onError(err.value(), data); + + if (emptyResult) + return onSuccess({}); + + if (data) + return onSuccess(data.value()); + else + return onError("Error reading request result", data); +} diff --git a/API/requests/request.h b/API/requests/request.h new file mode 100644 index 0000000..f287e76 --- /dev/null +++ b/API/requests/request.h @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace Request { +class Request : public QObject { + Q_OBJECT + +public: + Request (const QUrl& url); + virtual ~Request (); + + void send (QNetworkAccessManager& manager); + void cancel (); + void setAuthorizationToken(const QString& token); + +signals: + void done (); + void success (const QVariantMap& data); + void error (const QString& error, const std::optional& data); + +protected: + virtual void aquireRequest (QNetworkAccessManager& manager); + virtual void initializeRequest (); + virtual void onSuccess (const QVariantMap& data); + virtual void onError (const QString& error, const std::optional& data); + + std::optional validate (); + std::optional readResult (); + + static bool validateResponse (const std::optional& data, const std::map& structure); + static QUrl createUrl(QUrl base, const QString& path); + bool stateTerminal() const; + + constexpr static const char * const json = "application/json"; + constexpr static const char * const urlEncoded = "application/x-www-form-urlencoded"; + +private slots: + void onFinished (); + +protected: + static const std::map resultStructure; + enum class State { + initial, + sent, + responded, + cancelled + }; + struct NetworkReplyDeleter { + void operator () (QNetworkReply* reply) { + reply->deleteLater(); + } + }; + + State state; + QNetworkRequest request; + std::unique_ptr reply; + bool emptyResult; +}; +} diff --git a/API/requests/test.cpp b/API/requests/test.cpp new file mode 100644 index 0000000..52563a8 --- /dev/null +++ b/API/requests/test.cpp @@ -0,0 +1,27 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "test.h" + +const std::map Request::Test::structure({ + {"type", QMetaType::QString}, {"version", QMetaType::QString}, +}); + +Request::Test::Test (const QString& path): + Request(path + "/test") +{} + +void Request::Test::onSuccess (const QVariantMap& data) { + if (!validateResponse(data, structure)) + return onError("Malformed response", data); + + QString type = data.value("type").toString(); + if (type != "pica") + return onError("server of this type (" + type + ") is not supported", data); + + QString version = data.value("version").toString(); + if (version != "0.0.1") + return onError("server of this version (" + version + ") is not supported", data); + + Request::onSuccess(data); +} diff --git a/API/requests/test.h b/API/requests/test.h new file mode 100644 index 0000000..2303a78 --- /dev/null +++ b/API/requests/test.h @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2023 Yury Gubich +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "request.h" + +namespace Request { + +class Test : public Request { + Q_OBJECT + +public: + Test (const QString& path); + +protected: + void onSuccess (const QVariantMap &data) override; + +private: + static const std::map structure; +}; + +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 806c8a9..1370203 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,9 @@ endif() add_subdirectory(qml) add_subdirectory(API) +add_subdirectory(models) +target_include_directories(magpie PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(magpie PRIVATE Qt6::Core Qt6::Gui diff --git a/API/models/CMakeLists.txt b/models/CMakeLists.txt similarity index 87% rename from API/models/CMakeLists.txt rename to models/CMakeLists.txt index 4284a3e..1d43315 100644 --- a/API/models/CMakeLists.txt +++ b/models/CMakeLists.txt @@ -2,10 +2,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later set(HEADERS + magpie.h assets.h ) set(SOURCES + magpie.cpp assets.cpp ) diff --git a/API/models/assets.cpp b/models/assets.cpp similarity index 90% rename from API/models/assets.cpp rename to models/assets.cpp index 481adc8..fafcc37 100644 --- a/API/models/assets.cpp +++ b/models/assets.cpp @@ -3,7 +3,7 @@ #include "assets.h" -#include "../helpers.h" +#include "API/helpers.h" Models::Assets::Assets (QObject* parent): QAbstractListModel(parent), @@ -22,7 +22,7 @@ void Models::Assets::addAsset (const Asset& asset) { if (index.isValid()) throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model"); - beginInsertRows(QModelIndex(), records.size(), records.size() + 1); + beginInsertRows(QModelIndex(), records.size(), records.size()); records.push_back(asset); endInsertRows(); } @@ -35,7 +35,7 @@ void Models::Assets::addAssets (const std::deque& assets) { if (getIndex(asset.id).isValid()) throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model (bulk)"); - beginInsertRows(QModelIndex(), records.size(), records.size() + assets.size()); + beginInsertRows(QModelIndex(), records.size(), records.size() + assets.size() - 1); for (const Asset& asset : assets) records.push_back(asset); @@ -48,7 +48,7 @@ void Models::Assets::deleteAsset (unsigned int id) { throw std::runtime_error("An attempt to insert to delete non existing Asset from asset model"); int row = index.row(); - beginRemoveRows(QModelIndex(), row, row + 1); + beginRemoveRows(QModelIndex(), row, row); records.erase(records.begin() + row); if (state == State::syncronized) //give a second thought state = State::initial; @@ -68,7 +68,7 @@ int Models::Assets::rowCount (const QModelIndex& parent) const { QHash Models::Assets::roleNames () const { static const QHash roleNames{ - {Title, "title"}, {Icon, "icon"}, {Balance, "balance"}, {Archived, "archived"} + {Title, "title"}, {Icon, "icon"}, {Balance, "balance"}, {Archived, "archived"}, {Color, "color"} }; return roleNames; } @@ -101,6 +101,8 @@ QVariant Models::Assets::data (const QModelIndex& index, int role) const { return records[row].balance; case Archived: return records[row].archived; + case Color: + return records[row].color; } } @@ -118,6 +120,10 @@ bool Models::Assets::deserialize (const QVariantList& from, std::deque& o asset.icon = ser.value("icon").toString(); asset.archived = ser.value("archived").toBool(); asset.id = ser.value("id").toUInt(); + + uint32_t color = ser.value("color").toUInt(); + uint8_t* rgba = reinterpret_cast(&color); + asset.color = QColor::fromRgb(rgba[0], rgba[1], rgba[2], rgba[3]); } return true; @@ -135,5 +141,6 @@ QModelIndex Models::Assets::getIndex (unsigned int id) const { if (records[i].id == id) return createIndex(i, 0, &records[i]); } + return QModelIndex(); } diff --git a/API/models/assets.h b/models/assets.h similarity index 95% rename from API/models/assets.h rename to models/assets.h index 212a124..0109c11 100644 --- a/API/models/assets.h +++ b/models/assets.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -14,6 +15,7 @@ struct Asset { unsigned int id; QString title; QString icon; + QColor color; double balance; bool archived; unsigned int currency; @@ -22,7 +24,6 @@ struct Asset { class Assets : public QAbstractListModel { Q_OBJECT QML_ELEMENT - QML_SINGLETON public: explicit Assets (QObject* parent = nullptr); @@ -31,7 +32,8 @@ public: Title = Qt::UserRole + 1, Icon, Balance, - Archived + Archived, + Color }; void clear(); diff --git a/models/magpie.cpp b/models/magpie.cpp new file mode 100644 index 0000000..5baeec8 --- /dev/null +++ b/models/magpie.cpp @@ -0,0 +1,200 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#include "magpie.h" + +#include + +#include "API/api.h" +#include "API/helpers.h" +#include "API/codes.h" + +Models::Magpie::Magpie (QObject* parent): + QObject(parent), + assets(), + address(), + state(State::Offline), + accessToken(), + renewToken(), + api(), + firstPoll(), + pollRequestId(API::none) +{ + firstPoll.setSingleShot(true); + firstPoll.setInterval(2000); + connect(&firstPoll, &QTimer::timeout, this, &Magpie::onFirstPollTimerSuccess); + connect(&assets, &Assets::requestAssets, this, &Magpie::requestAssets); +} + +QUrl Models::Magpie::getAddress () const { + return address; +} + +Models::Magpie::State Models::Magpie::getState () const { + return state; +} + +QString Models::Magpie::getAccessToken () const { + return accessToken; +} + +QString Models::Magpie::getRenewToken () const { + return renewToken; +} + +void Models::Magpie::installAPI (const std::shared_ptr& api) { + Magpie::api = api; +} + +void Models::Magpie::setAddress (const QUrl& address) { + if (Magpie::address == address) + return; + + if (state == Authenticated) { + //do something + } + + Magpie::address = address; + emit addressChanged(address); + if (address.isEmpty()) + setState(NoServer); + else + setState(NotAuthenticated); +} + +void Models::Magpie::forceAddress (const QUrl& address) { + Magpie::address = ""; + setAddress(address); +} + +void Models::Magpie::setTokens (const QString access, const QString& renew, bool notify) { + accessToken = access; + renewToken = renew; + setState(Authenticating); + startPolling(); + + if (notify) + emit storeTokens(accessToken, renewToken); +} + +void Models::Magpie::setState (State newState) { + if (newState == state) + return; + + state = newState; + emit stateChanged(state); +} + +Models::Assets* Models::Magpie::getAssets () { + return &assets; +} + +void Models::Magpie::requestAssets () { + api->requestAssets( + [this] (const QVariantList& list) { + std::deque result; + bool res = Assets::deserialize(list, result); + if (!res) { + qDebug() << "Error deserializer received assets"; + result.clear(); + } else { + qDebug() << "Assets successfully received"; + } + + assets.addAssets(result); + }, + [this] (const QString& error, const std::optional& data) { + assets.addAssets({}); + } + ); +} + +void Models::Magpie::onFirstPollTimerSuccess () { + setState(Authenticated); +} + +void Models::Magpie::startPolling () { + qDebug() << "Starting polling..."; + if (state != Authenticating) + throw std::runtime_error("Can not start polling in this state: " + std::to_string(state)); + + sendPoll(); + firstPoll.start(); +} + +void Models::Magpie::sendPoll (bool clear) { + if (pollRequestId != API::none) + throw std::runtime_error("an attempt to send second poll request while the other one is active"); + + pollRequestId = api->poll( + std::bind(&Magpie::onPollSuccess, this, std::placeholders::_1), + std::bind(&Magpie::onPollError, this, std::placeholders::_1, std::placeholders::_2), + clear + ); +} + +void Models::Magpie::onPollSuccess (const QVariantMap& data) { + pollRequestId = API::none; + firstPoll.stop(); + + Codes::Poll code = Codes::convertPoll(data.value("result").toInt()); + bool clear = false; + switch (code) { + case Codes::Poll::success: + clear = handleChanges(qast(data.value("data"))); + //todo handle the result + case Codes::Poll::timeout: + setState(Authenticated); + return sendPoll(clear); + case Codes::Poll::tokenProblem: + case Codes::Poll::replace: + case Codes::Poll::unknownError: //todo this one doesn't actually mean that we can't work for now, the network may be temporarily down or something + return setState(NotAuthenticated); + } +} + +void Models::Magpie::onPollError (const QString& err, const std::optional& data) { + qDebug() << "Poll error:" << err; + pollRequestId = API::none; + firstPoll.stop(); + setState(NotAuthenticated); +} + +bool Models::Magpie::handleChanges (const QVariantMap& changes) { + QVariantMap::ConstIterator itr = changes.constFind("system"); + if (itr != changes.constEnd() && itr.value().canConvert()) { + const QVariantMap& sys = qast(itr.value()); + QVariantMap::ConstIterator invItr = sys.constFind("invalidate"); + if (invItr != sys.constEnd()) { + const QVariant& vinv = invItr.value(); + if (vinv.canConvert() && vinv.toBool()) + resetAllModels(); + } + } + + itr = changes.constFind("assets"); + if (itr != changes.constEnd() && itr.value().canConvert()) { + const QVariantMap& assets = qast(itr.value()); + QVariantMap::ConstIterator aItr = assets.constFind("invalidate"); + if (aItr != assets.constEnd()) { + const QVariant& vinv = aItr.value(); + if (vinv.canConvert() && vinv.toBool()) + Magpie::assets.clear(); + } + + aItr = assets.constFind("added"); + if (aItr != assets.constEnd() && aItr.value().canConvert()) { + std::deque added; + if (!Models::Assets::deserialize(qast(aItr.value()), added)) + qDebug() << "Error deserializng added assets"; + else + Magpie::assets.addAssets(added); + } + } + + return true; +} + +void Models::Magpie::resetAllModels () { + assets.clear(); +} diff --git a/models/magpie.h b/models/magpie.h new file mode 100644 index 0000000..5a299af --- /dev/null +++ b/models/magpie.h @@ -0,0 +1,81 @@ +//SPDX-FileCopyrightText: 2023 Yury Gubich +//SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include +#include +#include + +#include "assets.h" + +class API; +namespace Models { + +class Magpie : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + enum State { + Offline, + NoServer, + NotAuthenticated, + Authenticating, + Authenticated + }; + Q_ENUM(State) + + Q_PROPERTY(QUrl address READ getAddress NOTIFY addressChanged) + Q_PROPERTY(State state READ getState NOTIFY stateChanged) + Q_PROPERTY(Assets* assets READ getAssets CONSTANT) + + explicit Magpie(QObject *parent = nullptr); + + QUrl getAddress() const; + State getState() const; + QString getAccessToken() const; + QString getRenewToken() const; + + void installAPI(const std::shared_ptr& api); + void setAddress(const QUrl& address); + void forceAddress(const QUrl& address); + void setTokens(const QString access, const QString& renew, bool notify = false); + void setState(State newState); + Assets* getAssets(); + +signals: + void addressChanged(const QUrl& path); + void stateChanged(State state); + + void storeTokens(const QString& access, const QString& renew); + +public: + Assets assets; + +private slots: + void requestAssets(); + void onFirstPollTimerSuccess(); + +private: + void startPolling(); + void sendPoll(bool clear = false); + void onPollSuccess(const QVariantMap& data); + void onPollError(const QString& err, const std::optional& data); + bool handleChanges(const QVariantMap& changes); + void resetAllModels(); + +private: + QUrl address; + State state; + QString accessToken; + QString renewToken; + std::shared_ptr api; + QTimer firstPoll; + unsigned int pollRequestId; +}; + +} diff --git a/qml/Application/Assets.qml b/qml/Application/Assets.qml index 2ad654c..8440593 100644 --- a/qml/Application/Assets.qml +++ b/qml/Application/Assets.qml @@ -5,7 +5,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import magpie.Models as Models +import magpie import magpie.Components as Components Item { @@ -28,9 +28,11 @@ Item { id: listView Layout.fillHeight: true Layout.fillWidth: true - model: Models.Assets + model: Magpie.assets + spacing: 5 delegate: Components.AssetLine { height: 20 + width: listView.width } } } diff --git a/qml/Components/AssetLine.qml b/qml/Components/AssetLine.qml index a0a5ee9..eb39161 100644 --- a/qml/Components/AssetLine.qml +++ b/qml/Components/AssetLine.qml @@ -4,23 +4,50 @@ import QtQuick import QtQuick.Controls -Rectangle { +Item { id: line required property string title required property string icon + required property color color + required property string balance Row { + readonly property int iconSize: height + readonly property int freespace: width - iconSize - spacing * children.length - 1 + anchors.fill: parent - IconLabel { - anchors.verticalCenter: parent.verticalCenter - icon.name: line.icon - width: parent.height - height: parent.height + spacing: 5 + + Rectangle { + width: parent.iconSize + height: parent.iconSize + color: line.color + + IconLabel { + anchors.fill: parent + icon { + name: line.icon + width: width + height: height + } + } } Text { - anchors.verticalCenter: parent.verticalCenter + width: parent.freespace / 2 + height: parent.height text: title + verticalAlignment: Text.AlignVCenter + color: palette.text + font.bold: true + } + + Text { + width: parent.freespace / 2 + height: parent.height + text: balance + verticalAlignment: Text.AlignVCenter + color: palette.text } } } diff --git a/qml/Forms/AddAsset.qml b/qml/Forms/AddAsset.qml index 52f528b..f08321a 100644 --- a/qml/Forms/AddAsset.qml +++ b/qml/Forms/AddAsset.qml @@ -4,7 +4,7 @@ import QtQuick import QtQuick.Controls -import magpie.API +import magpie import magpie.Components as Components Item { diff --git a/qml/Forms/Login.qml b/qml/Forms/Login.qml index f2438d5..af697db 100644 --- a/qml/Forms/Login.qml +++ b/qml/Forms/Login.qml @@ -4,7 +4,7 @@ import QtQuick import QtQuick.Controls -import magpie.API +import magpie import magpie.Components as Components Column { diff --git a/qml/Forms/Register.qml b/qml/Forms/Register.qml index c4e31dd..5b36bf3 100644 --- a/qml/Forms/Register.qml +++ b/qml/Forms/Register.qml @@ -4,7 +4,7 @@ import QtQuick import QtQuick.Controls -import magpie.API +import magpie import magpie.Components as Components Column { diff --git a/qml/ServerPick.qml b/qml/ServerPick.qml index eea0fc5..43a9e49 100644 --- a/qml/ServerPick.qml +++ b/qml/ServerPick.qml @@ -5,7 +5,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import magpie.API +import magpie import magpie.Components as Components Page { diff --git a/qml/Welcome.qml b/qml/Welcome.qml index e8ebcbe..2b3eca1 100644 --- a/qml/Welcome.qml +++ b/qml/Welcome.qml @@ -5,7 +5,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import magpie.API +import magpie import magpie.Forms as Forms import magpie.Components as Components @@ -44,7 +44,7 @@ Page { } Label { horizontalAlignment: Label.AlignLeft - text: API.state === API.NoServer ? qsTr("choose") : API.address + text: Magpie.state === Magpie.NoServer ? qsTr("choose") : Magpie.address font { italic: true underline: true @@ -52,20 +52,20 @@ Page { MouseArea { anchors.fill: parent - onClicked: pickServer(API.address) + onClicked: pickServer(Magpie.address) } } } Components.Modal { - inProgress: API.state === API.Authenticating - visible: !priv.loggingIn && API.state === API.Authenticating - closable: API.state === API.NotAuthenticated - status: "Logging into " + API.address + "..." + inProgress: Magpie.state === Magpie.Authenticating + visible: !priv.loggingIn && Magpie.state === Magpie.Authenticating + closable: Magpie.state === Magpie.NotAuthenticated + status: "Logging into " + Magpie.address + "..." } Item { - visible: priv.loggingIn || API.state === API.NotAuthenticated || API.state === API.Authenticating + visible: priv.loggingIn || Magpie.state === Magpie.NotAuthenticated || Magpie.state === Magpie.Authenticating width: page.width height: stack.currentItem ? stack.currentItem.implicitHeight: 0 diff --git a/qml/main.qml b/qml/main.qml index d45c550..9dbce82 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -7,7 +7,7 @@ import QtQuick.Controls import QtQuick.Layouts import QtCore -import magpie.API +import magpie import magpie.Application as Application ApplicationWindow { @@ -35,7 +35,7 @@ ApplicationWindow { Component { id: serverPick ServerPick { - address: API.address + address: Magpie.address onBack: stack.pop() StackView.onActivating: pickingServer = true; StackView.onDeactivating: pickingServer = false; @@ -52,15 +52,15 @@ ApplicationWindow { } Connections { - target: API + target: Magpie function onAddressChanged (url) { if (pickingServer && url.toString().length > 0) stack.pop() } function onStateChanged (state) { - if (state === API.Authenticated) + if (state === Magpie.Authenticated) stack.push(app); - else if (runningApp && state === API.NotAuthenticated) + else if (runningApp && state === Magpie.NotAuthenticated) stack.pop(); } } diff --git a/root.cpp b/root.cpp index 5226e39..5b0cbfc 100644 --- a/root.cpp +++ b/root.cpp @@ -10,7 +10,8 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) : root(root), engine(), context(engine.rootContext()), - api() + magpie(), + api(std::make_shared(magpie)) { std::cout << "Starting Magpie..." << std::endl; @@ -21,26 +22,26 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) : setApplicationVersion("0.0.1"); setDesktopFileName("magpie"); + magpie.installAPI(api); + connect(&engine, &QQmlApplicationEngine::objectCreated, this, &Root::onObjectCreated, Qt::QueuedConnection ); QSettings settings; - api.setAddress(settings.value("address").toUrl()); + magpie.setAddress(settings.value("address").toUrl()); QString acc = settings.value("accessToken").toString(); QString ren = settings.value("renewToken").toString(); if (!acc.isEmpty() && !ren.isEmpty()) - api.setTokens(acc, ren); + magpie.setTokens(acc, ren); - qRegisterMetaType("API"); - connect(&api, &API::addressChanged, this, &Root::onAPIAddressChanged); - connect(&api, &API::storeTokens, this, &Root::onStoreTokens); + connect(&magpie, &Models::Magpie::addressChanged, this, &Root::onAPIAddressChanged); + connect(&magpie, &Models::Magpie::storeTokens, this, &Root::onStoreTokens); - qmlRegisterSingletonInstance("magpie.API", 1, 0, "API", &api); - - qmlRegisterSingletonInstance("magpie.Models", 1, 0, "Assets", &api.assets); + qmlRegisterSingletonInstance("magpie", 1, 0, "API", api.get()); + qmlRegisterSingletonInstance("magpie", 1, 0, "Magpie", &magpie); engine.addImportPath(":/"); engine.load(root); @@ -64,7 +65,7 @@ bool Root::notify(QObject* receiver, QEvent* e) { } std::cout << "Magpie is quiting..." << std::endl; - exit(1); + exit(-1); return false; } diff --git a/root.h b/root.h index eb51e7f..ea3b182 100644 --- a/root.h +++ b/root.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -12,6 +13,7 @@ #include #include +#include "models/magpie.h" #include "API/api.h" class Root : public QGuiApplication { @@ -30,5 +32,7 @@ private: QUrl root; QQmlApplicationEngine engine; QQmlContext* context; - API api; + Models::Magpie magpie; + std::shared_ptr api; + };