478 lines
16 KiB
C++
478 lines
16 KiB
C++
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
#include "api.h"
|
|
#include <QDebug>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QUrlQuery>
|
|
|
|
#include "codes.h"
|
|
#include "finalaction.h"
|
|
#include "helpers.h"
|
|
|
|
constexpr const char* json = "application/json";
|
|
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),
|
|
address(address),
|
|
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);
|
|
}
|
|
|
|
QUrl API::getAddress() const {
|
|
return address;
|
|
}
|
|
|
|
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<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;
|
|
if (state == Offline)
|
|
return callCallback(finished, "Need to be online to test");
|
|
|
|
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<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(
|
|
pollReply.get(), &QNetworkReply::finished,
|
|
this, &API::onPollFinished, Qt::QueuedConnection
|
|
);
|
|
}
|
|
|
|
void API::onPollFinished() {
|
|
firstPoll.stop();
|
|
QNetworkReply::NetworkError error = pollReply->error();
|
|
std::optional<QVariantMap> data = readResult(pollReply.get());
|
|
Codes::Poll code = Codes::Poll::unknownError;
|
|
if (validateResponse(data, resultStructure))
|
|
code = Codes::convertPoll(data->value("result").toInt());
|
|
else
|
|
qDebug() << "";
|
|
|
|
QString detail = Codes::description(code);
|
|
|
|
if (error != QNetworkReply::NoError)
|
|
qDebug() << pollReply->errorString() + ": " + detail;
|
|
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) {
|
|
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())
|
|
API::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
|
|
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...";
|
|
|
|
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)
|
|
);
|
|
}
|
|
|
|
void API::responseAssets(QNetworkReply *reply) {
|
|
std::deque<Models::Asset> result;
|
|
FinalAction action([this, &result]() {
|
|
assets.receivedAssets(result);
|
|
});
|
|
|
|
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);
|
|
if (!data) {
|
|
qDebug() << "Error receiving assets: bad data";
|
|
return;
|
|
}
|
|
|
|
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 {
|
|
if (callback.isCallable()) {
|
|
if (error.isEmpty())
|
|
callback.call(QJSValueList({QJSValue(QJSValue::NullValue)}) + arguments);
|
|
else
|
|
callback.call({QJSValue(error)});
|
|
}
|
|
}
|
|
|