big refactoring of API system

This commit is contained in:
Blue 2024-01-20 18:17:21 -03:00
parent 27124380e4
commit 7a116bfdf2
Signed by: blue
GPG Key ID: 9B203B252A63EE38
37 changed files with 1060 additions and 534 deletions

View File

@ -17,4 +17,4 @@ set(SOURCES
target_sources(magpie PRIVATE ${SOURCES}) target_sources(magpie PRIVATE ${SOURCES})
add_subdirectory(models) add_subdirectory(requests)

View File

@ -7,463 +7,125 @@
#include <QJsonObject> #include <QJsonObject>
#include <QUrlQuery> #include <QUrlQuery>
#include "codes.h" #include "requests/test.h"
#include "finalaction.h" #include "requests/register.h"
#include "helpers.h" #include "requests/login.h"
#include "requests/poll.h"
#include "requests/listassets.h"
#include "requests/addasset.h"
constexpr const char* json = "application/json"; API::API (Models::Magpie& magpie, QObject* parent):
constexpr const char* urlEncoded = "application/x-www-form-urlencoded";
const std::map<QString, QMetaType::Type> testStructure({
{"type", QMetaType::QString},
{"version", QMetaType::QString},
});
const std::map<QString, QMetaType::Type> resultStructure({
{"result", QMetaType::LongLong},
});
const std::map<QString, QMetaType::Type> tokensStructure({
{"accessToken", QMetaType::QString},
{"renewToken", QMetaType::QString}
});
struct NetworkReplyDeleter {
void operator () (QNetworkReply* reply) {
reply->deleteLater();
}
};
API::API(const QUrl& address, QObject* parent):
QObject(parent), QObject(parent),
address(address), idCounter(0),
magpie(magpie),
network(), network(),
state(NoServer),
accessToken(),
renewToken(),
firstPoll(),
pollReply(), pollReply(),
assets() requests()
{ {}
firstPoll.setSingleShot(true);
firstPoll.setInterval(2000); void API::cancelRequest (RequestId id) {
connect(&firstPoll, &QTimer::timeout, this, &API::onFirstPollSuccess); requests.erase(id);
connect(&assets, &Models::Assets::requestAssets, this, &API::requestAssets);
} }
QUrl API::getAddress() const { void API::onRequestDone (RequestId id) {
return address; requests.erase(id);
} }
API::State API::getState() const { API::RequestId API::test (const QString& path, const QJSValue& finished) {
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<QVariantMap> 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<QVariantMap>& data, const std::map<QString, QMetaType::Type>& structure) {
if (!data.has_value())
return false;
for (const std::pair<const QString, QMetaType::Type>& 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) {
qDebug() << "Testing" << path; qDebug() << "Testing" << path;
if (state == Offline) if (magpie.getState() == Models::Magpie::Offline)
return callCallback(finished, "Need to be online to test"); callCallback(finished, "Need to be online to test"), 0;
QUrl address(path); std::unique_ptr<Request::Test> test = std::make_unique<Request::Test>(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<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
if (error != QNetworkReply::NoError)
return callCallback(finished, reply->errorString());
std::optional<QVariantMap> 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<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
std::optional<QVariantMap> data = readResult(reply);
std::optional<Codes::Register> 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<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
std::optional<QVariantMap> data = readResult(reply);
std::optional<Codes::Login> 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<QNetworkReply, NetworkReplyDeleter> 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<QNetworkReply>(network.get(request));
connect( connect(
pollReply.get(), &QNetworkReply::finished, test.get(),
this, &API::onPollFinished, Qt::QueuedConnection &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() { API::RequestId API::sendRegister (const QString& login, const QString& password, const QJSValue& finished) {
firstPoll.stop(); qDebug() << "Registering...";
QNetworkReply::NetworkError error = pollReply->error(); if (magpie.getState() != Models::Magpie::NotAuthenticated)
std::optional<QVariantMap> data = readResult(pollReply.get()); return callCallback(finished, "Can not register in current state"), 0;
Codes::Poll code = Codes::Poll::unknownError;
if (validateResponse(data, resultStructure))
code = Codes::convertPoll(data->value("result").toInt());
else
qDebug() << "";
QString detail = Codes::description(code); std::unique_ptr<Request::Register> reg = std::make_unique<Request::Register>(login, password, magpie.getAddress());
connect(reg.get(), &Request::Register::success, std::bind(&API::callCallback, this, finished, QString(), QJSValueList{QJSValue(true)}));
if (error != QNetworkReply::NoError) connect(reg.get(), &Request::Register::error, std::bind(&API::callCallback, this, finished, std::placeholders::_1, QJSValueList{QJSValue(false)}));
qDebug() << pollReply->errorString() + ": " + detail; return registerAndSend(std::move(reg));
else
qDebug() << "Poll finished: " + detail;
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<QVariantMap>())
clear = handleChanges(qast<QVariantMap>(itr.value()));
else
qDebug("received a poll that claimed to have some updates, but the \"data\" field is not an object");
}
//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) { API::RequestId API::sendLogin (const QString& login, const QString& password, const QJSValue& finished) {
QVariantMap::ConstIterator itr = changes.constFind("system"); qDebug() << "Logging in...";
if (itr != changes.constEnd() && itr.value().canConvert<QVariantMap>()) { if (magpie.getState() != Models::Magpie::NotAuthenticated)
const QVariantMap& sys = qast<QVariantMap>(itr.value()); return callCallback(finished, "Can not register in current state"), 0;
QVariantMap::ConstIterator invItr = sys.constFind("invalidate");
if (invItr != sys.constEnd()) { std::unique_ptr<Request::Login> log = std::make_unique<Request::Login>(login, password, magpie.getAddress());
const QVariant& vinv = invItr.value(); connect(
if (vinv.canConvert<bool>() && vinv.toBool()) log.get(),
resetAllModels(); &Request::Login::success,
this,
[this, &finished] (const QVariantMap& data) {
callCallback(finished, QString(), {QJSValue(true)});
magpie.setTokens(data.value("accessToken").toString(), data.value("renewToken").toString());
} }
);
connect(
log.get(),
&Request::Login::error,
this,
[this, &finished] (const QString& error) {
callCallback(finished, error, {QJSValue(false)});
magpie.setState(Models::Magpie::NotAuthenticated);
}
);
magpie.setState(Models::Magpie::Authenticating);
return registerAndSend(std::move(log));
} }
itr = changes.constFind("assets"); API::RequestId API::addAsset (const QString& title, const QString& icon, const QJSValue& finished) {
if (itr != changes.constEnd() && itr.value().canConvert<QVariantMap>()) { qDebug() << "Adding asset...";
const QVariantMap& assets = qast<QVariantMap>(itr.value()); if (magpie.getState() != Models::Magpie::Authenticated)
QVariantMap::ConstIterator aItr = assets.constFind("invalidate"); return callCallback(finished, "Can not add assets in current state"), 0;
if (aItr != assets.constEnd()) {
const QVariant& vinv = aItr.value(); auto add = std::make_unique<Request::AddAsset>(title, icon, QColor::fromString("black"), 1, magpie.getAddress());
if (vinv.canConvert<bool>() && vinv.toBool()) add->setAuthorizationToken(magpie.getAccessToken());
API::assets.clear(); 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));
} }
aItr = assets.constFind("added"); API::RequestId API::requestAssets (const SuccessListHandler& success, const ErrorHandler& error) {
if (aItr != assets.constEnd() && aItr.value().canConvert<QVariantList>()) {
std::deque<Models::Asset> added;
if (!Models::Assets::deserialize(qast<QVariantList>(aItr.value()), added))
qDebug() << "Error deserializng added assets";
else
API::assets.addAssets(added);
}
}
return true;
}
void API::resetAllModels() {
assets.clear();
}
void API::onFirstPollSuccess() {
setState(Authenticated);
}
void API::requestAssets() {
if (state != Authenticated) {
qDebug() << "An attempt to request assets on unauthenticated state";
assets.receivedAssets({});
return;
}
qDebug() << "Requesting assets..."; qDebug() << "Requesting assets...";
QUrl url = createUrl("/listAssets"); auto list = std::make_unique<Request::ListAssets>(magpie.getAddress());
QNetworkRequest request(url); list->setAuthorizationToken(magpie.getAccessToken());
request.setHeader(QNetworkRequest::ContentTypeHeader, json); connect(list.get(), &Request::ListAssets::success, success);
request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8()); connect(list.get(), &Request::ListAssets::error, error);
return registerAndSend(std::move(list));
QNetworkReply* reply = network.get(request);
connect(
reply, &QNetworkReply::finished,
this, std::bind(&API::responseAssets, this, reply)
);
} }
void API::responseAssets(QNetworkReply *reply) { API::RequestId API::poll (const SuccessMapHandler& success, const ErrorHandler& error, bool clear) {
std::deque<Models::Asset> result; auto poll = std::make_unique<Request::Poll>(magpie.getAddress(), clear);
FinalAction action([this, &result]() { poll->setAuthorizationToken(magpie.getAccessToken());
assets.receivedAssets(result); connect(poll.get(), &Request::Poll::success, success);
}); connect(poll.get(), &Request::Poll::error, error);
return registerAndSend(std::move(poll));
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
if (error != QNetworkReply::NoError) {
qDebug() << "Error receiving assets:" << reply->errorString();
return;
} }
std::optional<QVariantMap> data = readResult(reply); API::RequestId API::registerAndSend (std::unique_ptr<Request::Request> request) {
if (!data) { Request::Request* req = request.get();
qDebug() << "Error receiving assets: bad data"; requests.emplace(++idCounter, std::move(request));
return; connect(req, &Request::Request::done, std::bind(&API::onRequestDone, this, idCounter));
} req->send(network);
return idCounter;
QVariantMap::ConstIterator itr = data->find("assets");
if (itr == data->constEnd() || !itr->canConvert<QVariantList>()) {
qDebug() << "Error receiving assets: assets are missing or not in an array";
return;
}
if (!Models::Assets::deserialize(qast<QVariantList>(itr.value()), result)) {
qDebug() << "Error deserializng assets";
result.clear();
}
qDebug() << "Assets successfully received";
//final action goes automatically
} }
void API::callCallback (const QJSValue& callback, const QString& error, const QJSValueList& arguments) const { void API::callCallback (const QJSValue& callback, const QString& error, const QJSValueList& arguments) const {
@ -474,4 +136,3 @@ void API::callCallback(const QJSValue& callback, const QString& error, const QJS
callback.call({QJSValue(error)}); callback.call({QJSValue(error)});
} }
} }

View File

@ -6,6 +6,7 @@
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <map> #include <map>
#include <functional>
#include <QObject> #include <QObject>
#include <QString> #include <QString>
@ -13,80 +14,45 @@
#include <QJSValue> #include <QJSValue>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include <QTimer>
#include "models/assets.h" #include "models/magpie.h"
#include "requests/request.h"
class Root;
class API : public QObject { class API : public QObject {
friend class Root;
Q_OBJECT 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: public:
explicit API(const QUrl& path = QString(), QObject* parent = nullptr); using SuccessMapHandler = std::function<void(const QVariantMap&)>;
using SuccessListHandler = std::function<void(const QVariantList&)>;
using ErrorHandler = std::function<void(const QString&, const std::optional<QVariantMap>&)>;
using RequestId = unsigned int;
QUrl getAddress() const; explicit API(Models::Magpie& magpie, QObject* parent = nullptr);
State getState() const;
void setTokens(const QString access, const QString& renew);
void startPolling();
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: public slots:
void test(const QString& path, const QJSValue& finished = QJSValue()); void cancelRequest(RequestId id);
void sendRegister(const QString& login, const QString& password, const QJSValue& finished = QJSValue()); RequestId test(const QString& path, const QJSValue& finished = QJSValue());
void sendLogin(const QString& login, const QString& password, const QJSValue& finished = QJSValue()); RequestId sendRegister(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
void addAsset(const QString& title, const QString& icon, 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: private slots:
void onTestFinished(QNetworkReply* reply, const QUrl& addr, const QJSValue& finished); void onRequestDone(RequestId id);
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);
private: private:
void callCallback(const QJSValue& callback, const QString& error = QString(), const QJSValueList& arguments = QJSValueList()) const; void callCallback(const QJSValue& callback, const QString& error = QString(), const QJSValueList& arguments = QJSValueList()) const;
void setAddress(const QUrl& path); RequestId registerAndSend(std::unique_ptr<Request::Request> request);
static std::optional<QVariantMap> readResult(QNetworkReply* reply);
static bool validateResponse(const std::optional<QVariantMap>& data, const std::map<QString, QMetaType::Type>& structure);
void setState(State newState);
QUrl createUrl(const QString& path) const;
void sendPoll(bool clear = false);
bool handleChanges(const QVariantMap& changes);
void resetAllModels();
private: private:
QUrl address; RequestId idCounter;
Models::Magpie& magpie;
QNetworkAccessManager network; QNetworkAccessManager network;
State state; std::map<RequestId, std::unique_ptr<Request::Request>> requests;
QString accessToken;
QString renewToken;
QTimer firstPoll;
std::unique_ptr<QNetworkReply> pollReply; std::unique_ptr<QNetworkReply> pollReply;
public:
Models::Assets assets;
}; };

View File

@ -6,6 +6,8 @@
#include <QVariant> #include <QVariant>
#include <stdexcept> #include <stdexcept>
#define UNUSED(X) (void)(X)
template <class T> template <class T>
const T& qast(const QVariant& variant) { const T& qast(const QVariant& variant) {
if (variant.userType() == qMetaTypeId<T>()) if (variant.userType() == qMetaTypeId<T>())

View File

@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
# 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})

10
API/requests/addasset.cpp Normal file
View File

@ -0,0 +1,10 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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;
}

19
API/requests/addasset.h Normal file
View File

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QColor>
#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);
};
}

View File

@ -0,0 +1,18 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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<QVariantList>())
return Request::onError("Error receiving assets: assets are missing or not in an array", std::nullopt);
emit success(qast<QVariantList>(itr.value()));
emit done();
}

23
API/requests/listassets.h Normal file
View File

@ -0,0 +1,23 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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;
};
}

34
API/requests/login.cpp Normal file
View File

@ -0,0 +1,34 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "login.h"
#include "API/codes.h"
const std::map<QString, QMetaType::Type> 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<QVariantMap>& data) {
if (validateResponse(data, resultStructure))
Request::onError(err + ": " + Codes::description(Codes::convertLogin(data->value("result").toInt())), data);
else
Request::onError(err, data);
}

23
API/requests/login.h Normal file
View File

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// 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<QVariantMap>& data) override;
static const std::map<QString, QMetaType::Type> tokensStructure;
};
}

46
API/requests/poll.cpp Normal file
View File

@ -0,0 +1,46 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "poll.h"
#include <QUrlQuery>
#include "API/codes.h"
const std::map<QString, QMetaType::Type> 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<QVariantMap>& 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;
}

25
API/requests/poll.h Normal file
View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// 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<QVariantMap>& data) override;
static QUrl createUrl(QUrl base, const QString& path, bool clear);
static const std::map<QString, QMetaType::Type> updatesStructure;
};
}

17
API/requests/post.cpp Normal file
View File

@ -0,0 +1,17 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//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<QNetworkReply, NetworkReplyDeleter>(manager.post(request, form.toString(QUrl::FullyEncoded).toUtf8()));
}
void Request::Post::initializeRequest () {
request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded);
}

25
API/requests/post.h Normal file
View File

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "request.h"
#include <QUrlQuery>
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;
};
}

29
API/requests/register.cpp Normal file
View File

@ -0,0 +1,29 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "register.h"
#include <QUrlQuery>
#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<QVariantMap>& data) {
if (validateResponse(data, resultStructure))
Request::onError(err + ": " + Codes::description(Codes::convertRegister(data->value("result").toInt())), data);
else
Request::onError(err, data);
}

21
API/requests/register.h Normal file
View File

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// 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<QVariantMap>& data) override;
};
}

134
API/requests/request.cpp Normal file
View File

@ -0,0 +1,134 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "request.h"
#include <QJsonDocument>
#include <QJsonObject>
const std::map<QString, QMetaType::Type> 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<QNetworkReply, NetworkReplyDeleter>(manager.get(request));
}
void Request::Request::initializeRequest () {
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
}
std::optional<QString> Request::Request::validate () {
QNetworkReply::NetworkError error = reply->error();
if (error != QNetworkReply::NoError)
return reply->errorString();
else
return std::nullopt;
}
std::optional<QVariantMap> Request::Request::readResult () {
QVariant contentType = reply->header(QNetworkRequest::ContentTypeHeader);
if (!contentType.isValid() || !contentType.canConvert<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();
}
void Request::Request::onSuccess (const QVariantMap& data) {
emit success(data);
emit done();
}
void Request::Request::onError (const QString& err, const std::optional<QVariantMap>& data) {
emit error(err, data);
emit done();
}
bool Request::Request::validateResponse (const std::optional<QVariantMap>& data, const std::map<QString, QMetaType::Type>& structure) {
if (!data.has_value())
return false;
for (const std::pair<const QString, QMetaType::Type>& 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<QString> err = validate();
std::optional<QVariantMap> 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);
}

69
API/requests/request.h Normal file
View File

@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include <optional>
#include <QObject>
#include <QString>
#include <QVariantMap>
#include <QNetworkReply>
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<QVariantMap>& 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<QVariantMap>& data);
std::optional<QString> validate ();
std::optional<QVariantMap> readResult ();
static bool validateResponse (const std::optional<QVariantMap>& data, const std::map<QString, QMetaType::Type>& 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<QString, QMetaType::Type> resultStructure;
enum class State {
initial,
sent,
responded,
cancelled
};
struct NetworkReplyDeleter {
void operator () (QNetworkReply* reply) {
reply->deleteLater();
}
};
State state;
QNetworkRequest request;
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> reply;
bool emptyResult;
};
}

27
API/requests/test.cpp Normal file
View File

@ -0,0 +1,27 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "test.h"
const std::map<QString, QMetaType::Type> 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);
}

23
API/requests/test.h Normal file
View File

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// 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<QString, QMetaType::Type> structure;
};
}

View File

@ -59,7 +59,9 @@ endif()
add_subdirectory(qml) add_subdirectory(qml)
add_subdirectory(API) add_subdirectory(API)
add_subdirectory(models)
target_include_directories(magpie PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(magpie PRIVATE target_link_libraries(magpie PRIVATE
Qt6::Core Qt6::Core
Qt6::Gui Qt6::Gui

View File

@ -2,10 +2,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
set(HEADERS set(HEADERS
magpie.h
assets.h assets.h
) )
set(SOURCES set(SOURCES
magpie.cpp
assets.cpp assets.cpp
) )

View File

@ -3,7 +3,7 @@
#include "assets.h" #include "assets.h"
#include "../helpers.h" #include "API/helpers.h"
Models::Assets::Assets (QObject* parent): Models::Assets::Assets (QObject* parent):
QAbstractListModel(parent), QAbstractListModel(parent),
@ -22,7 +22,7 @@ void Models::Assets::addAsset (const Asset& asset) {
if (index.isValid()) if (index.isValid())
throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model"); 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); records.push_back(asset);
endInsertRows(); endInsertRows();
} }
@ -35,7 +35,7 @@ void Models::Assets::addAssets (const std::deque<Asset>& assets) {
if (getIndex(asset.id).isValid()) if (getIndex(asset.id).isValid())
throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model (bulk)"); 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) for (const Asset& asset : assets)
records.push_back(asset); 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"); throw std::runtime_error("An attempt to insert to delete non existing Asset from asset model");
int row = index.row(); int row = index.row();
beginRemoveRows(QModelIndex(), row, row + 1); beginRemoveRows(QModelIndex(), row, row);
records.erase(records.begin() + row); records.erase(records.begin() + row);
if (state == State::syncronized) //give a second thought if (state == State::syncronized) //give a second thought
state = State::initial; state = State::initial;
@ -68,7 +68,7 @@ int Models::Assets::rowCount (const QModelIndex& parent) const {
QHash<int, QByteArray> Models::Assets::roleNames () const { QHash<int, QByteArray> Models::Assets::roleNames () const {
static const QHash<int, QByteArray> roleNames{ static const QHash<int, QByteArray> roleNames{
{Title, "title"}, {Icon, "icon"}, {Balance, "balance"}, {Archived, "archived"} {Title, "title"}, {Icon, "icon"}, {Balance, "balance"}, {Archived, "archived"}, {Color, "color"}
}; };
return roleNames; return roleNames;
} }
@ -101,6 +101,8 @@ QVariant Models::Assets::data (const QModelIndex& index, int role) const {
return records[row].balance; return records[row].balance;
case Archived: case Archived:
return records[row].archived; return records[row].archived;
case Color:
return records[row].color;
} }
} }
@ -118,6 +120,10 @@ bool Models::Assets::deserialize (const QVariantList& from, std::deque<Asset>& o
asset.icon = ser.value("icon").toString(); asset.icon = ser.value("icon").toString();
asset.archived = ser.value("archived").toBool(); asset.archived = ser.value("archived").toBool();
asset.id = ser.value("id").toUInt(); asset.id = ser.value("id").toUInt();
uint32_t color = ser.value("color").toUInt();
uint8_t* rgba = reinterpret_cast<uint8_t*>(&color);
asset.color = QColor::fromRgb(rgba[0], rgba[1], rgba[2], rgba[3]);
} }
return true; return true;
@ -135,5 +141,6 @@ QModelIndex Models::Assets::getIndex (unsigned int id) const {
if (records[i].id == id) if (records[i].id == id)
return createIndex(i, 0, &records[i]); return createIndex(i, 0, &records[i]);
} }
return QModelIndex(); return QModelIndex();
} }

View File

@ -6,6 +6,7 @@
#include <deque> #include <deque>
#include <QString> #include <QString>
#include <QColor>
#include <QAbstractListModel> #include <QAbstractListModel>
#include <qqmlregistration.h> #include <qqmlregistration.h>
@ -14,6 +15,7 @@ struct Asset {
unsigned int id; unsigned int id;
QString title; QString title;
QString icon; QString icon;
QColor color;
double balance; double balance;
bool archived; bool archived;
unsigned int currency; unsigned int currency;
@ -22,7 +24,6 @@ struct Asset {
class Assets : public QAbstractListModel { class Assets : public QAbstractListModel {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
QML_SINGLETON
public: public:
explicit Assets (QObject* parent = nullptr); explicit Assets (QObject* parent = nullptr);
@ -31,7 +32,8 @@ public:
Title = Qt::UserRole + 1, Title = Qt::UserRole + 1,
Icon, Icon,
Balance, Balance,
Archived Archived,
Color
}; };
void clear(); void clear();

200
models/magpie.cpp Normal file
View File

@ -0,0 +1,200 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "magpie.h"
#include <QDebug>
#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>& 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<Asset> 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<QVariantMap>& 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<QVariantMap>(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<QVariantMap>& 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<QVariantMap>()) {
const QVariantMap& sys = qast<QVariantMap>(itr.value());
QVariantMap::ConstIterator invItr = sys.constFind("invalidate");
if (invItr != sys.constEnd()) {
const QVariant& vinv = invItr.value();
if (vinv.canConvert<bool>() && vinv.toBool())
resetAllModels();
}
}
itr = changes.constFind("assets");
if (itr != changes.constEnd() && itr.value().canConvert<QVariantMap>()) {
const QVariantMap& assets = qast<QVariantMap>(itr.value());
QVariantMap::ConstIterator aItr = assets.constFind("invalidate");
if (aItr != assets.constEnd()) {
const QVariant& vinv = aItr.value();
if (vinv.canConvert<bool>() && vinv.toBool())
Magpie::assets.clear();
}
aItr = assets.constFind("added");
if (aItr != assets.constEnd() && aItr.value().canConvert<QVariantList>()) {
std::deque<Models::Asset> added;
if (!Models::Assets::deserialize(qast<QVariantList>(aItr.value()), added))
qDebug() << "Error deserializng added assets";
else
Magpie::assets.addAssets(added);
}
}
return true;
}
void Models::Magpie::resetAllModels () {
assets.clear();
}

81
models/magpie.h Normal file
View File

@ -0,0 +1,81 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory.h>
#include <QObject>
#include <QQmlEngine>
#include <QTimer>
#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>& 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<QVariantMap>& data);
bool handleChanges(const QVariantMap& changes);
void resetAllModels();
private:
QUrl address;
State state;
QString accessToken;
QString renewToken;
std::shared_ptr<API> api;
QTimer firstPoll;
unsigned int pollRequestId;
};
}

View File

@ -5,7 +5,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import magpie.Models as Models import magpie
import magpie.Components as Components import magpie.Components as Components
Item { Item {
@ -28,9 +28,11 @@ Item {
id: listView id: listView
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
model: Models.Assets model: Magpie.assets
spacing: 5
delegate: Components.AssetLine { delegate: Components.AssetLine {
height: 20 height: 20
width: listView.width
} }
} }
} }

View File

@ -4,23 +4,50 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
Rectangle { Item {
id: line id: line
required property string title required property string title
required property string icon required property string icon
required property color color
required property string balance
Row { Row {
readonly property int iconSize: height
readonly property int freespace: width - iconSize - spacing * children.length - 1
anchors.fill: parent anchors.fill: parent
spacing: 5
Rectangle {
width: parent.iconSize
height: parent.iconSize
color: line.color
IconLabel { IconLabel {
anchors.verticalCenter: parent.verticalCenter anchors.fill: parent
icon.name: line.icon icon {
width: parent.height name: line.icon
height: parent.height width: width
height: height
}
}
} }
Text { Text {
anchors.verticalCenter: parent.verticalCenter width: parent.freespace / 2
height: parent.height
text: title 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
} }
} }
} }

View File

@ -4,7 +4,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import magpie.API import magpie
import magpie.Components as Components import magpie.Components as Components
Item { Item {

View File

@ -4,7 +4,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import magpie.API import magpie
import magpie.Components as Components import magpie.Components as Components
Column { Column {

View File

@ -4,7 +4,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import magpie.API import magpie
import magpie.Components as Components import magpie.Components as Components
Column { Column {

View File

@ -5,7 +5,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import magpie.API import magpie
import magpie.Components as Components import magpie.Components as Components
Page { Page {

View File

@ -5,7 +5,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import magpie.API import magpie
import magpie.Forms as Forms import magpie.Forms as Forms
import magpie.Components as Components import magpie.Components as Components
@ -44,7 +44,7 @@ Page {
} }
Label { Label {
horizontalAlignment: Label.AlignLeft horizontalAlignment: Label.AlignLeft
text: API.state === API.NoServer ? qsTr("choose") : API.address text: Magpie.state === Magpie.NoServer ? qsTr("choose") : Magpie.address
font { font {
italic: true italic: true
underline: true underline: true
@ -52,20 +52,20 @@ Page {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: pickServer(API.address) onClicked: pickServer(Magpie.address)
} }
} }
} }
Components.Modal { Components.Modal {
inProgress: API.state === API.Authenticating inProgress: Magpie.state === Magpie.Authenticating
visible: !priv.loggingIn && API.state === API.Authenticating visible: !priv.loggingIn && Magpie.state === Magpie.Authenticating
closable: API.state === API.NotAuthenticated closable: Magpie.state === Magpie.NotAuthenticated
status: "Logging into " + API.address + "..." status: "Logging into " + Magpie.address + "..."
} }
Item { 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 width: page.width
height: stack.currentItem ? stack.currentItem.implicitHeight: 0 height: stack.currentItem ? stack.currentItem.implicitHeight: 0

View File

@ -7,7 +7,7 @@ import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtCore import QtCore
import magpie.API import magpie
import magpie.Application as Application import magpie.Application as Application
ApplicationWindow { ApplicationWindow {
@ -35,7 +35,7 @@ ApplicationWindow {
Component { Component {
id: serverPick id: serverPick
ServerPick { ServerPick {
address: API.address address: Magpie.address
onBack: stack.pop() onBack: stack.pop()
StackView.onActivating: pickingServer = true; StackView.onActivating: pickingServer = true;
StackView.onDeactivating: pickingServer = false; StackView.onDeactivating: pickingServer = false;
@ -52,15 +52,15 @@ ApplicationWindow {
} }
Connections { Connections {
target: API target: Magpie
function onAddressChanged (url) { function onAddressChanged (url) {
if (pickingServer && url.toString().length > 0) if (pickingServer && url.toString().length > 0)
stack.pop() stack.pop()
} }
function onStateChanged (state) { function onStateChanged (state) {
if (state === API.Authenticated) if (state === Magpie.Authenticated)
stack.push(app); stack.push(app);
else if (runningApp && state === API.NotAuthenticated) else if (runningApp && state === Magpie.NotAuthenticated)
stack.pop(); stack.pop();
} }
} }

View File

@ -10,7 +10,8 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) :
root(root), root(root),
engine(), engine(),
context(engine.rootContext()), context(engine.rootContext()),
api() magpie(),
api(std::make_shared<API>(magpie))
{ {
std::cout << "Starting Magpie..." << std::endl; std::cout << "Starting Magpie..." << std::endl;
@ -21,26 +22,26 @@ Root::Root(const QUrl& root, int& argc, char* argv[]) :
setApplicationVersion("0.0.1"); setApplicationVersion("0.0.1");
setDesktopFileName("magpie"); setDesktopFileName("magpie");
magpie.installAPI(api);
connect(&engine, &QQmlApplicationEngine::objectCreated, connect(&engine, &QQmlApplicationEngine::objectCreated,
this, &Root::onObjectCreated, this, &Root::onObjectCreated,
Qt::QueuedConnection Qt::QueuedConnection
); );
QSettings settings; QSettings settings;
api.setAddress(settings.value("address").toUrl()); magpie.setAddress(settings.value("address").toUrl());
QString acc = settings.value("accessToken").toString(); QString acc = settings.value("accessToken").toString();
QString ren = settings.value("renewToken").toString(); QString ren = settings.value("renewToken").toString();
if (!acc.isEmpty() && !ren.isEmpty()) if (!acc.isEmpty() && !ren.isEmpty())
api.setTokens(acc, ren); magpie.setTokens(acc, ren);
qRegisterMetaType<API>("API"); connect(&magpie, &Models::Magpie::addressChanged, this, &Root::onAPIAddressChanged);
connect(&api, &API::addressChanged, this, &Root::onAPIAddressChanged); connect(&magpie, &Models::Magpie::storeTokens, this, &Root::onStoreTokens);
connect(&api, &API::storeTokens, this, &Root::onStoreTokens);
qmlRegisterSingletonInstance("magpie.API", 1, 0, "API", &api); qmlRegisterSingletonInstance("magpie", 1, 0, "API", api.get());
qmlRegisterSingletonInstance("magpie", 1, 0, "Magpie", &magpie);
qmlRegisterSingletonInstance("magpie.Models", 1, 0, "Assets", &api.assets);
engine.addImportPath(":/"); engine.addImportPath(":/");
engine.load(root); engine.load(root);
@ -64,7 +65,7 @@ bool Root::notify(QObject* receiver, QEvent* e) {
} }
std::cout << "Magpie is quiting..." << std::endl; std::cout << "Magpie is quiting..." << std::endl;
exit(1); exit(-1);
return false; return false;
} }

6
root.h
View File

@ -5,6 +5,7 @@
#include <iostream> #include <iostream>
#include <stdexcept> #include <stdexcept>
#include <memory>
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
@ -12,6 +13,7 @@
#include <QQmlApplicationEngine> #include <QQmlApplicationEngine>
#include <QQmlContext> #include <QQmlContext>
#include "models/magpie.h"
#include "API/api.h" #include "API/api.h"
class Root : public QGuiApplication { class Root : public QGuiApplication {
@ -30,5 +32,7 @@ private:
QUrl root; QUrl root;
QQmlApplicationEngine engine; QQmlApplicationEngine engine;
QQmlContext* context; QQmlContext* context;
API api; Models::Magpie magpie;
std::shared_ptr<API> api;
}; };