Skip to content

Commit

Permalink
Bring githubpp here and add a standalone preset.
Browse files Browse the repository at this point in the history
  • Loading branch information
Holt59 committed Jul 14, 2024
1 parent 4681286 commit c510c01
Show file tree
Hide file tree
Showing 7 changed files with 341 additions and 13 deletions.
15 changes: 9 additions & 6 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@
"inherits": ["cmake-dev", "vcpkg"],
"name": "vs2022-windows",
"toolset": "v143"
}
],
"buildPresets": [
},
{
"name": "vs2022-windows",
"resolvePackageReferences": "on",
"configurePreset": "vs2022-windows"
"cacheVariables": {
"VCPKG_MANIFEST_FEATURES": {
"type": "STRING",
"value": "standalone"
}
},
"inherits": "vs2022-windows",
"name": "vs2022-windows-standalone"
}
],
"version": 4
Expand Down
10 changes: 6 additions & 4 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ find_package(usvfs CONFIG REQUIRED)
find_package(mo2-uibase CONFIG REQUIRED)
find_package(mo2-archive CONFIG REQUIRED)
find_package(mo2-lootcli-header CONFIG REQUIRED)
find_package(mo2-githubpp CONFIG REQUIRED)
find_package(mo2-bsatk CONFIG REQUIRED)
find_package(mo2-esptk CONFIG REQUIRED)
find_package(mo2-dds-header CONFIG REQUIRED)
Expand All @@ -20,14 +19,16 @@ find_package(lz4 CONFIG REQUIRED)
find_package(ZLIB REQUIRED)

add_executable(organizer)
set_target_properties(organizer PROPERTIES OUTPUT_NAME "ModOrganizer")
set_target_properties(organizer PROPERTIES
OUTPUT_NAME "ModOrganizer"
WIN32_EXECUTABLE TRUE)
mo2_configure_target(organizer WARNINGS OFF)
mo2_set_project_to_run_from_install(
organizer EXECUTABLE ${CMAKE_INSTALL_PREFIX}/bin/ModOrganizer.exe)

target_link_libraries(organizer PRIVATE
Shlwapi Bcrypt
usvfs::usvfs mo2::uibase mo2::archive mo2::githubpp mo2::libbsarch
usvfs::usvfs mo2::uibase mo2::archive mo2::libbsarch
mo2::bsatk mo2::esptk mo2::lootcli-header
Boost::program_options Boost::signals2 Boost::uuid Boost::accumulators
Qt6::WebEngineWidgets Qt6::WebSockets Version Dbghelp)
Expand Down Expand Up @@ -55,7 +56,7 @@ install(FILES $<TARGET_FILE:mo2::libbsarch> DESTINATION bin/dlls)
install(FILES $<TARGET_FILE:mo2::archive> DESTINATION bin/dlls)
install(FILES $<TARGET_FILE:7zip::7zip> DESTINATION bin/dlls)

mo2_deploy_qt(BINARIES ModOrganizer.exe uibase.dll)
mo2_deploy_qt(BINARIES ModOrganizer.exe $<TARGET_FILE_NAME:mo2::uibase>)

mo2_add_filter(NAME src/application GROUPS
iuserinterface
Expand Down Expand Up @@ -83,6 +84,7 @@ mo2_add_filter(NAME src/categories GROUPS

mo2_add_filter(NAME src/core GROUPS
archivefiletree
githubpp
installationmanager
nexusinterface
nxmaccessmanager
Expand Down
204 changes: 204 additions & 0 deletions src/github.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#include "github.h"
#include <QEventLoop>
#include <QJsonDocument>
#include <QNetworkRequest>

#include <QCoreApplication>
#include <QThread>

static const QString GITHUB_URL("https://api.github.com");
static const QString USER_AGENT("GitHubPP");

GitHub::GitHub(const char* clientId) : m_AccessManager(new QNetworkAccessManager(this))
{}

GitHub::~GitHub()
{
// delete all the replies since they depend on the access manager, which is
// about to be deleted
for (auto* reply : m_replies) {
reply->disconnect();
delete reply;
}
}

QJsonArray GitHub::releases(const Repository& repo)
{
QJsonDocument result = request(
Method::GET, QString("repos/%1/%2/releases").arg(repo.owner, repo.project),
QByteArray(), true);
return result.array();
}

void GitHub::releases(const Repository& repo,
const std::function<void(const QJsonArray&)>& callback)
{
request(
Method::GET, QString("repos/%1/%2/releases").arg(repo.owner, repo.project),
QByteArray(),
[callback](const QJsonDocument& result) {
callback(result.array());
},
true);
}

QJsonDocument GitHub::handleReply(QNetworkReply* reply)
{
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode != 200) {
return QJsonDocument(QJsonObject(
{{"http_status", statusCode},
{"redirection",
reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString()},
{"reason",
reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()}}));
}

QByteArray data = reply->readAll();
if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) {
return QJsonDocument();
}

QJsonParseError parseError;
QJsonDocument result = QJsonDocument::fromJson(data, &parseError);

if (parseError.error != QJsonParseError::NoError) {
return QJsonDocument(QJsonObject({{"parse_error", parseError.errorString()}}));
}

return result;
}

QNetworkReply* GitHub::genReply(Method method, const QString& path,
const QByteArray& data, bool relative)
{
QNetworkRequest request(relative ? GITHUB_URL + "/" + path : path);

request.setHeader(QNetworkRequest::UserAgentHeader, USER_AGENT);
request.setRawHeader("Accept", "application/vnd.github.v3+json");

switch (method) {
case Method::GET:
return m_AccessManager->get(request);
case Method::POST:
return m_AccessManager->post(request, data);
default:
// this shouldn't be possible as all enum options are handled
throw std::runtime_error("invalid method");
}
}

QJsonDocument GitHub::request(Method method, const QString& path,
const QByteArray& data, bool relative)
{
QEventLoop wait;
QNetworkReply* reply = genReply(method, path, data, relative);

connect(reply, SIGNAL(finished), &wait, SLOT(quit()));
wait.exec();
QJsonDocument result = handleReply(reply);
reply->deleteLater();

QJsonObject object = result.object();
if (object.value("http_status").toDouble() == 301.0) {
return request(method, object.value("redirection").toString(), data, false);
} else {
return result;
}
}

void GitHub::request(Method method, const QString& path, const QByteArray& data,
const std::function<void(const QJsonDocument&)>& callback,
bool relative)
{
// make sure the timer is owned by this so it's deleted correctly and
// doesn't fire after the GitHub object is destroyed; this happens when
// restarting MO by switching instances, for example
QTimer* timer = new QTimer(this);
timer->setSingleShot(true);
timer->setInterval(10000);

QNetworkReply* reply = genReply(method, path, data, relative);

// remember this reply so it can be deleted in the destructor if necessary
m_replies.push_back(reply);

Request req = {method, data, callback, timer, reply};

// finished
connect(reply, &QNetworkReply::finished, [this, req] {
onFinished(req);
});

// error
connect(reply, qOverload<QNetworkReply::NetworkError>(&QNetworkReply::errorOccurred),
[this, req](auto&& error) {
onError(req, error);
});

// timeout
connect(timer, &QTimer::timeout, [this, req] {
onTimeout(req);
});

timer->start();
}

void GitHub::onFinished(const Request& req)
{
QJsonDocument result = handleReply(req.reply);
QJsonObject object = result.object();

req.timer->stop();

if (object.value("http_status").toInt() == 301) {
request(req.method, object.value("redirection").toString(), req.data, req.callback,
false);
} else {
req.callback(result);
}

deleteReply(req.reply);
}

void GitHub::onError(const Request& req, QNetworkReply::NetworkError error)
{
// the only way the request can be aborted is when there's a timeout, which
// already logs a message
if (error != QNetworkReply::OperationCanceledError) {
qCritical().noquote().nospace()
<< "Github: request for " << req.reply->url().toString() << " failed, "
<< req.reply->errorString() << " (" << error << ")";
}

req.timer->stop();
req.reply->disconnect();

QJsonObject root({{"network_error", req.reply->errorString()}});
QJsonDocument doc(root);

req.callback(doc);

deleteReply(req.reply);
}

void GitHub::onTimeout(const Request& req)
{
qCritical().noquote().nospace()
<< "Github: request for " << req.reply->url().toString() << " timed out";

// don't delete the reply, abort will fire the error() handler above
req.reply->abort();
}

void GitHub::deleteReply(QNetworkReply* reply)
{
// remove from the list
auto itor = std::find(m_replies.begin(), m_replies.end(), reply);
if (itor != m_replies.end()) {
m_replies.erase(itor);
}

// delete
reply->deleteLater();
}
108 changes: 108 additions & 0 deletions src/github.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#pragma once

#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkCookieJar>
#include <QNetworkReply>
#include <QTimer>
#include <functional>

class GitHubException : public std::exception
{
public:
GitHubException(const QJsonObject& errorObj) : std::exception()
{
initMessage(errorObj);
}

virtual ~GitHubException() throw() override {}

virtual const char* what() const throw() { return m_Message.constData(); }

private:
void initMessage(const QJsonObject& obj)
{
if (obj.contains("http_status")) {
m_Message = QString("HTTP Status %1: %2")
.arg(obj.value("http_status").toInt())
.arg(obj.value("reason").toString())
.toUtf8();
} else if (obj.contains("parse_error")) {
m_Message = QString("Parsing failed: %1")
.arg(obj.value("parse_error").toString())
.toUtf8();
} else if (obj.contains("network_error")) {
m_Message = QString("Network failed: %1")
.arg(obj.value("network_error").toString())
.toUtf8();
} else {
m_Message = "Unknown error";
}
}

QByteArray m_Message;
};

class GitHub : public QObject
{

Q_OBJECT

public:
enum class Method
{
GET,
POST
};

struct Repository
{
Repository(const QString& owner, const QString& project)
: owner(owner), project(project)
{}
QString owner;
QString project;
};

public:
GitHub(const char* clientId = nullptr);
~GitHub();

QJsonArray releases(const Repository& repo);
void releases(const Repository& repo,
const std::function<void(const QJsonArray&)>& callback);

private:
QJsonDocument request(Method method, const QString& path, const QByteArray& data,
bool relative);
void request(Method method, const QString& path, const QByteArray& data,
const std::function<void(const QJsonDocument&)>& callback,
bool relative);

QJsonDocument handleReply(QNetworkReply* reply);
QNetworkReply* genReply(Method method, const QString& path, const QByteArray& data,
bool relative);

private:
struct Request
{
Method method = Method::GET;
QByteArray data;
std::function<void(const QJsonDocument&)> callback;
QTimer* timer = nullptr;
QNetworkReply* reply = nullptr;
};

QNetworkAccessManager* m_AccessManager;

// remember the replies that are in flight and delete them in the destructor
std::vector<QNetworkReply*> m_replies;

void onFinished(const Request& req);
void onError(const Request& req, QNetworkReply::NetworkError error);
void onTimeout(const Request& req);

void deleteReply(QNetworkReply* reply);
};
2 changes: 1 addition & 1 deletion src/selfupdater.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>.

#include <map>

#include <githubpp/github.h>
#include <github.h>
#include <uibase/versioninfo.h>

class Archive;
Expand Down
2 changes: 1 addition & 1 deletion vcpkg-configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
{
"kind": "git",
"repository": "https://github.com/ModOrganizer2/vcpkg-registry",
"baseline": "a05c478c9ded48f3bed4fce5f3cc7a2be3dcda49",
"baseline": "e415ce268bc9502222f42372da415063bfd11c3e",
"packages": ["mo2-*", "7zip"]
}
]
Expand Down
Loading

0 comments on commit c510c01

Please sign in to comment.