-
Notifications
You must be signed in to change notification settings - Fork 165
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bring githubpp here and add a standalone preset.
- Loading branch information
Showing
7 changed files
with
341 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.