magpie/API/api.cpp

333 lines
10 KiB
C++
Raw Normal View History

// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
2023-11-24 23:48:01 +00:00
#include "api.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
2023-12-16 01:44:25 +00:00
#include <QUrlQuery>
2023-11-24 23:48:01 +00:00
#include "codes.h"
#include "finalaction.h"
2023-11-24 23:48:01 +00:00
constexpr const char* json = "application/json";
2023-12-16 01:44:25 +00:00
constexpr const char* urlEncoded = "application/x-www-form-urlencoded";
2023-11-24 23:48:01 +00:00
2023-12-25 20:07:51 +00:00
const std::map<QString, QMetaType::Type> testStructure({
{"type", QMetaType::QString},
{"version", QMetaType::QString},
});
const std::map<QString, QMetaType::Type> resultStructure({
{"result", QMetaType::LongLong},
2023-12-25 20:07:51 +00:00
});
const std::map<QString, QMetaType::Type> tokensStructure({
{"accessToken", QMetaType::QString},
{"renewToken", QMetaType::QString}
});
2023-11-24 23:48:01 +00:00
struct NetworkReplyDeleter {
void operator () (QNetworkReply* reply) {
reply->deleteLater();
}
};
API::API(const QUrl& address, QObject* parent):
QObject(parent),
address(address),
2023-12-16 01:44:25 +00:00
network(),
state(NoServer),
accessToken(),
renewToken(),
firstPoll(),
pollReply()
{
firstPoll.setSingleShot(true);
firstPoll.setInterval(2000);
connect(&firstPoll, &QTimer::timeout, this, &API::onFirstPollSuccess);
}
2023-11-24 23:48:01 +00:00
2023-12-16 01:44:25 +00:00
QUrl API::getAddress() const {
return address;
}
API::State API::getState() const {
return state;
}
2023-12-25 20:07:51 +00:00
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();
2023-12-25 20:07:51 +00:00
}
2023-12-16 01:44:25 +00:00
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);
2023-11-24 23:48:01 +00:00
}
2023-12-25 20:07:51 +00:00
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::sendPoll() {
if (accessToken.isEmpty())
throw std::runtime_error("Can not start polling: access token is empty");
QByteArray authorizationHeader = "Bearer " + accessToken.toUtf8();
QNetworkRequest request(createUrl("/poll"));
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
request.setRawHeader("Authorization", authorizationHeader);
request.setTransferTimeout(30000);
pollReply = std::unique_ptr<QNetworkReply>(network.get(request));
connect(
pollReply.get(), &QNetworkReply::finished,
this, &API::onPollFinished,
Qt::QueuedConnection
);
}
2023-11-24 23:48:01 +00:00
void API::test(const QString& path, const QJSValue& finished) {
qDebug() << "Testing" << path;
2023-12-17 00:06:04 +00:00
if (state == Offline)
return callCallback(finished, "Need to be online to test");
2023-12-16 01:44:25 +00:00
2023-12-17 00:06:04 +00:00
QUrl address(path);
2023-11-24 23:48:01 +00:00
QNetworkRequest request(path + "/info");
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
QNetworkReply* reply = network.get(request);
connect(reply, &QNetworkReply::finished,
2023-12-17 00:06:04 +00:00
std::bind(&API::onTestFinished, this, reply, address, finished)
2023-11-24 23:48:01 +00:00
);
}
2023-12-17 00:06:04 +00:00
void API::onTestFinished(QNetworkReply* reply, const QUrl& addr, const QJSValue& finished) {
2023-11-24 23:48:01 +00:00
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
2023-12-17 00:06:04 +00:00
if (error != QNetworkReply::NoError)
return callCallback(finished, reply->errorString());
2023-11-24 23:48:01 +00:00
2023-12-25 20:07:51 +00:00
std::optional<QVariantMap> data = readResult(reply);
if (!validateResponse(data, testStructure))
return callCallback(finished, "Malformed response");
2023-11-24 23:48:01 +00:00
2023-12-25 20:07:51 +00:00
QString type = data->value("type").toString();
if (type != "pica")
return callCallback(finished, "server of this type (" + type + ") is not supported");
2023-11-24 23:48:01 +00:00
2023-12-25 20:07:51 +00:00
QString version = data->value("version").toString();
if (version != "0.0.1")
return callCallback(finished, "server of this version (" + version + ") is not supported");
2023-11-24 23:48:01 +00:00
callCallback(finished, QString(), {QJSValue(true)});
2023-12-17 00:06:04 +00:00
address = ""; //to provoke singal change even if it's the same server
setAddress(addr);
2023-11-24 23:48:01 +00:00
}
void API::sendRegister(const QString& login, const QString& password, const QJSValue& finished) {
2023-12-16 01:44:25 +00:00
qDebug() << "Registering...";
2023-12-17 00:06:04 +00:00
if (state != NotAuthenticated)
return callCallback(finished, "Can not register in current state");
2023-12-16 01:44:25 +00:00
QUrlQuery params({
{"login", login},
{"password", password}
});
QNetworkRequest request(createUrl("/register"));
2023-12-16 01:44:25 +00:00
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::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::onRegisterFinished(QNetworkReply* reply, const QJSValue& finished) const {
2023-12-16 01:44:25 +00:00
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
2023-12-25 20:07:51 +00:00
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());
2023-12-16 01:44:25 +00:00
}
2023-12-25 20:07:51 +00:00
if (error != QNetworkReply::NoError)
return callCallback(finished, reply->errorString() + (success ? "" : ": " + detail));
2023-12-16 01:44:25 +00:00
2023-12-25 20:07:51 +00:00
if (!code)
return callCallback(finished, "Malformed result");
2023-12-16 01:44:25 +00:00
2023-12-25 20:07:51 +00:00
callCallback(finished, detail, {QJSValue(success)});
}
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();
2023-12-25 20:07:51 +00:00
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)
2023-12-25 20:07:51 +00:00
return callCallback(finished, reply->errorString() + (success ? "" : ": " + detail));
2023-12-25 20:07:51 +00:00
if (!code)
return callCallback(finished, "Malformed result");
if (!validateResponse(data, tokensStructure))
return callCallback(finished, "Malformed result: missing tokens");
2023-12-25 20:07:51 +00:00
callCallback(finished, detail, {QJSValue(success)});
state = Authenticating;
2023-12-25 20:07:51 +00:00
accessToken = data->value("accessToken").toString();
renewToken = data->value("renewToken").toString();
emit storeTokens(accessToken, renewToken);
startPolling();
}
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());
QString detail = Codes::description(code);
if (error != QNetworkReply::NoError)
qDebug() << pollReply->errorString() + ": " + detail;
else
qDebug() << "Poll finished: " + detail;
switch (code) {
case Codes::Poll::success:
//todo handle the result
case Codes::Poll::timeout:
return sendPoll();
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);
}
}
void API::onFirstPollSuccess() {
setState(Authenticated);
2023-12-16 01:44:25 +00:00
}
2023-11-24 23:48:01 +00:00
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)});
}
}