Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ add_subdirectory(services)
if (BLUETOOTH)
add_subdirectory(bluetooth)
endif()

add_subdirectory(http)
21 changes: 21 additions & 0 deletions src/http/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
qt_add_library(quickshell-http STATIC
client.cpp
response.cpp
)

qt_add_qml_module(quickshell-http
URI Quickshell.Http
VERSION 0.1
DEPENDENCIES QtQml
)

install_qml_module(quickshell-http)

target_link_libraries(quickshell-http PRIVATE
Qt::Qml
Qt::Network
)

qs_module_pch(quickshell-http)

target_link_libraries(quickshell PRIVATE quickshell-http)
218 changes: 218 additions & 0 deletions src/http/client.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
#include "client.hpp"

#include <qjsengine.h>
#include <qjsvalue.h>
#include <qjsvalueiterator.h>
#include <qnetworkaccessmanager.h>
#include <qnetworkreply.h>
#include <qnetworkrequest.h>
#include <qobject.h>
#include <qstringview.h>
#include <qtimer.h>
#include <qurl.h>

#include "response.hpp"

namespace qs::http {

HttpClient::HttpClient(QObject* parent)
: QObject(parent)
, mManager(new QNetworkAccessManager(this)) {}

QNetworkRequest HttpClient::createRequest(const QString& url, const QJSValue& headers) {
QNetworkRequest req;

req.setUrl(QUrl(url));

if (headers.isObject()) {
QJSValueIterator iter(headers);
while (iter.hasNext()) {
iter.next();
req.setRawHeader(iter.name().toUtf8(), iter.value().toString().toUtf8());
}
}

return req;
}

void HttpClient::connectReply(QNetworkReply* reply, const QJSValue& callback, int timeout) {
if (timeout > 0) {
auto* timer = new QTimer(reply);
timer->setSingleShot(true);
timer->setInterval(timeout);
connect(timer, &QTimer::timeout, reply, &QNetworkReply::abort);
timer->start();
}

QJSEngine* engine = qjsEngine(this);

if (!engine) {
return;
}
if (callback.isCallable()) {
connect(reply, &QNetworkReply::finished, this, [reply, engine, callback]() mutable {
reply->deleteLater();
auto* resp = new HttpResponse(reply);
auto jsResp = engine->newQObject(resp);
QJSEngine::setObjectOwnership(resp, QJSEngine::JavaScriptOwnership);

callback.call(QJSValueList() << jsResp);
});
}
}

void HttpClient::request(
const QString& url,
const QString& verb,
const QJSValue& options,
const QJSValue& callback
) {
QJSValue headers;
QByteArray body;
int timeout = -1;

if (options.isObject()) {
if (options.hasOwnProperty("headers")) {
headers = options.property("headers");
}
if (options.hasOwnProperty("body")) {
auto bodyProp = options.property("body");
body = bodyProp.toVariant().toByteArray();
}
if (options.hasOwnProperty("timeout")) {
auto timeoutProp = options.property("timeout");
if (timeoutProp.isNumber()) {
timeout = static_cast<int>(timeoutProp.toNumber());
}
}
}

auto req = this->createRequest(url, headers);
auto* reply = this->mManager->sendCustomRequest(req, verb.toUtf8(), body);

this->connectReply(reply, callback, timeout);
}

void HttpClient::get(const QString& url, const QJSValue& options, const QJSValue& callback) {
QJSValue headers;
int timeout = -1;

if (options.isObject()) {
if (options.hasOwnProperty("headers")) {
headers = options.property("headers");
}
if (options.hasOwnProperty("timeout")) {
auto timeoutProp = options.property("timeout");
if (timeoutProp.isNumber()) {
timeout = static_cast<int>(timeoutProp.toNumber());
}
}
}

auto req = this->createRequest(url, headers);
auto* reply = this->mManager->get(req);

this->connectReply(reply, callback, timeout);
}

void HttpClient::post(
const QString& url,
const QVariant& body,
const QJSValue& options,
const QJSValue& callback
) {
auto bodyBytes = body.toByteArray();
QJSValue headers;
int timeout = -1;

if (options.isObject()) {
if (options.hasOwnProperty("headers")) {
headers = options.property("headers");
}
if (options.hasOwnProperty("timeout")) {
auto timeoutProp = options.property("timeout");
if (timeoutProp.isNumber()) {
timeout = static_cast<int>(timeoutProp.toNumber());
}
}
}

auto req = this->createRequest(url, headers);
auto* reply = this->mManager->post(req, bodyBytes);

this->connectReply(reply, callback, timeout);
}

void HttpClient::put(
const QString& url,
const QVariant& body,
const QJSValue& options,
const QJSValue& callback
) {
auto bodyBytes = body.toByteArray();
QJSValue headers;
int timeout = -1;

if (options.isObject()) {
if (options.hasOwnProperty("headers")) {
headers = options.property("headers");
}
if (options.hasOwnProperty("timeout")) {
auto timeoutProp = options.property("timeout");
if (timeoutProp.isNumber()) {
timeout = static_cast<int>(timeoutProp.toNumber());
}
}
}

auto req = this->createRequest(url, headers);
auto* reply = this->mManager->put(req, bodyBytes);

this->connectReply(reply, callback, timeout);
}

void HttpClient::del(const QString& url, const QJSValue& options, const QJSValue& callback) {
QJSValue headers;
int timeout = -1;

if (options.isObject()) {
if (options.hasOwnProperty("headers")) {
headers = options.property("headers");
}
if (options.hasOwnProperty("timeout")) {
auto timeoutProp = options.property("timeout");
if (timeoutProp.isNumber()) {
timeout = static_cast<int>(timeoutProp.toNumber());
}
}
}

auto req = this->createRequest(url, headers);
auto* reply = this->mManager->deleteResource(req);

this->connectReply(reply, callback, timeout);
}

void HttpClient::head(const QString& url, const QJSValue& options, const QJSValue& callback) {
QJSValue headers;
int timeout = -1;

if (options.isObject()) {
if (options.hasOwnProperty("headers")) {
headers = options.property("headers");
}
if (options.hasOwnProperty("timeout")) {
auto timeoutProp = options.property("timeout");
if (timeoutProp.isNumber()) {
timeout = static_cast<int>(timeoutProp.toNumber());
}
}
}

auto req = this->createRequest(url, headers);
auto* reply = this->mManager->head(req);

this->connectReply(reply, callback, timeout);
}

} // namespace qs::http
54 changes: 54 additions & 0 deletions src/http/client.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#pragma once

#include <qcontainerfwd.h>
#include <qjsvalue.h>
#include <qnetworkaccessmanager.h>
#include <qnetworkreply.h>
#include <qnetworkrequest.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtmetamacros.h>

namespace qs::http {

class HttpClient: public QObject {
Q_OBJECT;
QML_ELEMENT;
QML_SINGLETON;

public:
explicit HttpClient(QObject* parent = nullptr);

QNetworkRequest createRequest(const QString& url, const QJSValue& headers);
void connectReply(QNetworkReply* reply, const QJSValue& callback, int timeout);

Q_INVOKABLE void request(
const QString& url,
const QString& verb,
const QJSValue& options,
const QJSValue& callback = QJSValue()
);
Q_INVOKABLE void
get(const QString& url, const QJSValue& options, const QJSValue& callback = QJSValue());
Q_INVOKABLE void post(
const QString& url,
const QVariant& body,
const QJSValue& options,
const QJSValue& callback = QJSValue()
);
Q_INVOKABLE void
put(const QString& url,
const QVariant& body,
const QJSValue& options,
const QJSValue& callback = QJSValue());
// cannot use delete since it's a reserved keyword
Q_INVOKABLE void
del(const QString& url, const QJSValue& options, const QJSValue& callback = QJSValue());
Q_INVOKABLE void
head(const QString& url, const QJSValue& options, const QJSValue& callback = QJSValue());

private:
QNetworkAccessManager* mManager;
};

} // namespace qs::http
8 changes: 8 additions & 0 deletions src/http/modules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
name = "Quickshell.Http"
description = "HTTP fetch API"
headers = [
"client.hpp",
"response.hpp",
]
-----
Quickshell's HTTP module.
58 changes: 58 additions & 0 deletions src/http/response.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include "response.hpp"

#include <qcontainerfwd.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonparseerror.h>
#include <qjsonvalue.h>
#include <qnetworkreply.h>
#include <qnetworkrequest.h>
#include <qobject.h>
#include <qqmlinfo.h>

namespace qs::http {

HttpResponse::HttpResponse(QNetworkReply* reply, QObject* parent): QObject(parent) {
this->mStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
this->mStatusText = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toString();
this->mUrl = reply->url();
this->mData = reply->readAll();

auto headersList = reply->rawHeaderList();
for (auto& header: headersList) {
this->mHeadersMap.insert(
QString::fromUtf8(header),
QString::fromUtf8(reply->rawHeader(header))
);
}
}

QUrl HttpResponse::url() { return this->mUrl; }

QString HttpResponse::text() { return QString::fromUtf8(this->mData); }

QJsonValue HttpResponse::json() {
QJsonParseError error;
auto json = QJsonDocument::fromJson(this->mData, &error);

if (error.error != QJsonParseError::NoError) {
qmlWarning(this) << "Failed to deserialize json: " << error.errorString();
return QJsonValue::Undefined;
}

if (json.isArray()) {
return json.array();
}

return json.object();
}

QByteArray HttpResponse::arrayBuffer() { return this->mData; }

QVariantMap HttpResponse::headers() { return this->mHeadersMap; }

QString HttpResponse::header(const QString& name) {
return this->mHeadersMap.value(name).toString();
}

} // namespace qs::http
Loading
Loading