From 6a418e6e59d295344db0b89ac7f63a7ce530ddd5 Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sat, 7 Jul 2018 11:08:57 +0000 Subject: [PATCH] Refactor NetworkRequest class Add followUser and unfollowUser methods to TwitchAccount --- chatterino.pro | 6 + src/common/NetworkCommon.hpp | 22 ++ src/common/NetworkData.cpp | 45 +++ src/common/NetworkData.hpp | 36 +++ src/common/NetworkRequest.cpp | 306 ++++++++++----------- src/common/NetworkRequest.hpp | 248 +++-------------- src/common/NetworkResult.cpp | 46 ++++ src/common/NetworkResult.hpp | 20 ++ src/common/NetworkTimer.hpp | 58 ++++ src/common/NetworkWorker.hpp | 4 +- src/common/UrlFetch.hpp | 141 +--------- src/messages/Image.cpp | 5 +- src/providers/bttv/BttvEmotes.cpp | 32 ++- src/providers/ffz/FfzEmotes.cpp | 32 ++- src/providers/twitch/PartialTwitchUser.cpp | 70 +++++ src/providers/twitch/PartialTwitchUser.hpp | 25 ++ src/providers/twitch/TwitchAccount.cpp | 89 ++++-- src/providers/twitch/TwitchAccount.hpp | 2 + src/providers/twitch/TwitchChannel.cpp | 53 +++- src/providers/twitch/TwitchEmotes.cpp | 44 +-- src/singletons/Resources.cpp | 88 +++--- src/singletons/Updates.cpp | 4 +- src/widgets/dialogs/LoginDialog.cpp | 9 +- src/widgets/dialogs/LogsPopup.cpp | 14 +- src/widgets/dialogs/UserInfoPopup.cpp | 57 ++-- src/widgets/splits/Split.cpp | 29 +- src/widgets/splits/SplitHeader.cpp | 19 +- 27 files changed, 835 insertions(+), 669 deletions(-) create mode 100644 src/common/NetworkCommon.hpp create mode 100644 src/common/NetworkData.cpp create mode 100644 src/common/NetworkData.hpp create mode 100644 src/common/NetworkResult.cpp create mode 100644 src/common/NetworkResult.hpp create mode 100644 src/common/NetworkTimer.hpp create mode 100644 src/providers/twitch/PartialTwitchUser.cpp create mode 100644 src/providers/twitch/PartialTwitchUser.hpp diff --git a/chatterino.pro b/chatterino.pro index f149affcf..0716cd707 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -103,6 +103,8 @@ SOURCES += \ src/common/CompletionModel.cpp \ src/common/Emotemap.cpp \ src/common/NetworkManager.cpp \ + src/common/NetworkResult.cpp \ + src/common/NetworkData.cpp \ src/common/NetworkRequest.cpp \ src/controllers/accounts/Account.cpp \ src/controllers/accounts/AccountController.cpp \ @@ -138,6 +140,7 @@ SOURCES += \ src/providers/irc/IrcConnection2.cpp \ src/providers/irc/IrcServer.cpp \ src/providers/twitch/IrcMessageHandler.cpp \ + src/providers/twitch/PartialTwitchUser.cpp \ src/providers/twitch/PubsubActions.cpp \ src/providers/twitch/PubsubHelpers.cpp \ src/providers/twitch/TwitchAccount.cpp \ @@ -239,6 +242,8 @@ HEADERS += \ src/common/LockedObject.hpp \ src/common/MutexValue.hpp \ src/common/NetworkManager.hpp \ + src/common/NetworkResult.hpp \ + src/common/NetworkData.hpp \ src/common/NetworkRequest.hpp \ src/common/NetworkRequester.hpp \ src/common/NetworkWorker.hpp \ @@ -294,6 +299,7 @@ HEADERS += \ src/providers/irc/IrcServer.hpp \ src/providers/twitch/EmoteValue.hpp \ src/providers/twitch/IrcMessageHandler.hpp \ + src/providers/twitch/PartialTwitchUser.hpp \ src/providers/twitch/PubsubActions.hpp \ src/providers/twitch/PubsubHelpers.hpp \ src/providers/twitch/TwitchAccount.hpp \ diff --git a/src/common/NetworkCommon.hpp b/src/common/NetworkCommon.hpp new file mode 100644 index 000000000..743eeee04 --- /dev/null +++ b/src/common/NetworkCommon.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include + +class QNetworkReply; + +namespace chatterino { + +class NetworkResult; + +using NetworkSuccessCallback = std::function; +using NetworkErrorCallback = std::function; +using NetworkReplyCreatedCallback = std::function; + +enum class NetworkRequestType { + Get, + Post, + Put, + Delete, +}; + +} // namespace chatterino diff --git a/src/common/NetworkData.cpp b/src/common/NetworkData.cpp new file mode 100644 index 000000000..878bda409 --- /dev/null +++ b/src/common/NetworkData.cpp @@ -0,0 +1,45 @@ +#include "common/NetworkData.hpp" + +#include "Application.hpp" +#include "singletons/Paths.hpp" + +#include +#include + +namespace chatterino { + +QString NetworkData::getHash() +{ + if (this->hash_.isEmpty()) { + QByteArray bytes; + + bytes.append(this->request_.url().toString()); + + for (const auto &header : this->request_.rawHeaderList()) { + bytes.append(header); + } + + QByteArray hashBytes(QCryptographicHash::hash(bytes, QCryptographicHash::Sha256)); + + this->hash_ = hashBytes.toHex(); + } + + return this->hash_; +} + +void NetworkData::writeToCache(const QByteArray &bytes) +{ + if (this->useQuickLoadCache_) { + auto app = getApp(); + + QFile cachedFile(app->paths->cacheDirectory + "/" + this->getHash()); + + if (cachedFile.open(QIODevice::WriteOnly)) { + cachedFile.write(bytes); + + cachedFile.close(); + } + } +} + +} // namespace chatterino diff --git a/src/common/NetworkData.hpp b/src/common/NetworkData.hpp new file mode 100644 index 000000000..d3cecc046 --- /dev/null +++ b/src/common/NetworkData.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "common/NetworkCommon.hpp" + +#include + +#include + +class QNetworkReply; + +namespace chatterino { + +class NetworkResult; + +struct NetworkData { + QNetworkRequest request_; + const QObject *caller_ = nullptr; + bool useQuickLoadCache_{}; + + NetworkReplyCreatedCallback onReplyCreated_; + NetworkErrorCallback onError_; + NetworkSuccessCallback onSuccess_; + + NetworkRequestType requestType_ = NetworkRequestType::Get; + + QByteArray payload_; + + QString getHash(); + + void writeToCache(const QByteArray &bytes); + +private: + QString hash_; +}; + +} // namespace chatterino diff --git a/src/common/NetworkRequest.cpp b/src/common/NetworkRequest.cpp index 640b477f9..74aac0931 100644 --- a/src/common/NetworkRequest.cpp +++ b/src/common/NetworkRequest.cpp @@ -1,62 +1,78 @@ #include "common/NetworkRequest.hpp" #include "Application.hpp" +#include "common/NetworkManager.hpp" +#include "providers/twitch/TwitchCommon.hpp" +#include "singletons/Paths.hpp" + +#include + +#include namespace chatterino { -NetworkRequest::NetworkRequest(const char *url) +NetworkRequest::NetworkRequest(const std::string &url, NetworkRequestType requestType) + : timer(new NetworkTimer) { - this->data.request.setUrl(QUrl(url)); + this->data.request_.setUrl(QUrl(QString::fromStdString(url))); + this->data.requestType_ = requestType; } -NetworkRequest::NetworkRequest(const std::string &url) +NetworkRequest::NetworkRequest(QUrl url, NetworkRequestType requestType) + : timer(new NetworkTimer) { - this->data.request.setUrl(QUrl(QString::fromStdString(url))); + this->data.request_.setUrl(url); + this->data.requestType_ = requestType; } -NetworkRequest::NetworkRequest(const QString &url) +NetworkRequest::~NetworkRequest() { - this->data.request.setUrl(QUrl(url)); + assert(this->executed_); } -NetworkRequest::NetworkRequest(QUrl url) +void NetworkRequest::setRequestType(NetworkRequestType newRequestType) { - this->data.request.setUrl(url); -} - -void NetworkRequest::setRequestType(RequestType newRequestType) -{ - this->data.requestType = newRequestType; + this->data.requestType_ = newRequestType; } void NetworkRequest::setCaller(const QObject *caller) { - this->data.caller = caller; + this->data.caller_ = caller; } -void NetworkRequest::setOnReplyCreated(std::function f) +void NetworkRequest::onReplyCreated(NetworkReplyCreatedCallback cb) { - this->data.onReplyCreated = f; + this->data.onReplyCreated_ = cb; +} + +void NetworkRequest::onError(NetworkErrorCallback cb) +{ + this->data.onError_ = cb; +} + +void NetworkRequest::onSuccess(NetworkSuccessCallback cb) +{ + this->data.onSuccess_ = cb; } void NetworkRequest::setRawHeader(const char *headerName, const char *value) { - this->data.request.setRawHeader(headerName, value); + this->data.request_.setRawHeader(headerName, value); } void NetworkRequest::setRawHeader(const char *headerName, const QByteArray &value) { - this->data.request.setRawHeader(headerName, value); + this->data.request_.setRawHeader(headerName, value); } void NetworkRequest::setRawHeader(const char *headerName, const QString &value) { - this->data.request.setRawHeader(headerName, value.toUtf8()); + this->data.request_.setRawHeader(headerName, value.toUtf8()); } void NetworkRequest::setTimeout(int ms) { - this->data.timeoutMS = ms; + this->timer->timeoutMS_ = ms; } void NetworkRequest::makeAuthorizedV5(const QString &clientID, const QString &oauthToken) @@ -68,190 +84,174 @@ void NetworkRequest::makeAuthorizedV5(const QString &clientID, const QString &oa } } -void NetworkRequest::setUseQuickLoadCache(bool value) +void NetworkRequest::setPayload(const QByteArray &payload) { - this->data.useQuickLoadCache = value; + this->data.payload_ = payload; } -void NetworkRequest::Data::writeToCache(const QByteArray &bytes) +void NetworkRequest::setUseQuickLoadCache(bool value) { - if (this->useQuickLoadCache) { - auto app = getApp(); - - QFile cachedFile(app->paths->cacheDirectory + "/" + this->getHash()); - - if (cachedFile.open(QIODevice::WriteOnly)) { - cachedFile.write(bytes); - - cachedFile.close(); - } - } + this->data.useQuickLoadCache_ = value; } void NetworkRequest::execute() { - switch (this->data.requestType) { - case GetRequest: { - this->executeGet(); + this->executed_ = true; + + switch (this->data.requestType_) { + case NetworkRequestType::Get: { + // Get requests try to load from cache, then perform the request + if (this->data.useQuickLoadCache_) { + if (this->tryLoadCachedFile()) { + Log("Loaded from cache"); + // Successfully loaded from cache + return; + } + } + + this->doRequest(); } break; - case PutRequest: { - this->executePut(); + case NetworkRequestType::Put: { + // Put requests cannot be cached, therefore the request is called immediately + this->doRequest(); } break; - case DeleteRequest: { - this->executeDelete(); + case NetworkRequestType::Delete: { + // Delete requests cannot be cached, therefore the request is called immediately + this->doRequest(); } break; default: { - Log("[Execute] Unhandled request type {}", (int)this->data.requestType); + Log("[Execute] Unhandled request type"); } break; } } -void NetworkRequest::useCache() +bool NetworkRequest::tryLoadCachedFile() { - if (this->data.useQuickLoadCache) { - auto app = getApp(); + auto app = getApp(); - QFile cachedFile(app->paths->cacheDirectory + "/" + this->data.getHash()); + QFile cachedFile(app->paths->cacheDirectory + "/" + this->data.getHash()); - if (cachedFile.exists()) { - if (cachedFile.open(QIODevice::ReadOnly)) { - QByteArray bytes = cachedFile.readAll(); - - // qDebug() << "Loaded cached resource" << this->data.request.url(); - - auto document = parseJSONFromData2(bytes); - - bool success = false; - - if (!document.IsNull()) { - success = this->data.onSuccess(document); - } - - cachedFile.close(); - - if (!success) { - // The images were not successfully loaded from the file - // XXX: Invalidate the cache file so we don't attempt to load it again next - // time - } - } - } + if (!cachedFile.exists()) { + // File didn't exist + return false; } + + if (!cachedFile.open(QIODevice::ReadOnly)) { + // File could not be opened + return false; + } + + QByteArray bytes = cachedFile.readAll(); + NetworkResult result(bytes); + + bool success = this->data.onSuccess_(result); + + cachedFile.close(); + + // XXX: If success is false, we should invalidate the cache file somehow/somewhere + + return success; } void NetworkRequest::doRequest() { - QTimer *timer = nullptr; - if (this->data.timeoutMS > 0) { - timer = new QTimer; - } - NetworkRequester requester; NetworkWorker *worker = new NetworkWorker; worker->moveToThread(&NetworkManager::workerThread); - if (this->data.caller != nullptr) { - QObject::connect(worker, &NetworkWorker::doneUrl, this->data.caller, - [data = this->data](auto reply) mutable { - if (reply->error() != QNetworkReply::NetworkError::NoError) { - if (data.onError) { - data.onError(reply->error()); - } - return; - } + this->timer->start(); - QByteArray readBytes = reply->readAll(); - QByteArray bytes; - bytes.setRawData(readBytes.data(), readBytes.size()); - data.writeToCache(bytes); - data.onSuccess(parseJSONFromData2(bytes)); + auto onUrlRequested = [data = std::move(this->data), timer = std::move(this->timer), + worker]() mutable { + QNetworkReply *reply = nullptr; + switch (data.requestType_) { + case NetworkRequestType::Get: { + reply = NetworkManager::NaM.get(data.request_); + } break; - reply->deleteLater(); - }); - } + case NetworkRequestType::Put: { + reply = NetworkManager::NaM.put(data.request_, data.payload_); + } break; - if (timer != nullptr) { - timer->start(this->data.timeoutMS); - } + case NetworkRequestType::Delete: { + reply = NetworkManager::NaM.deleteResource(data.request_); + } break; + } - QObject::connect(&requester, &NetworkRequester::requestUrl, worker, - [timer, data = std::move(this->data), worker]() { - QNetworkReply *reply = nullptr; - switch (data.requestType) { - case GetRequest: { - reply = NetworkManager::NaM.get(data.request); - } break; + if (reply == nullptr) { + Log("Unhandled request type"); + return; + } - case PutRequest: { - reply = NetworkManager::NaM.put(data.request, data.payload); - } break; + if (timer->isStarted()) { + timer->onTimeout(worker, [reply, data]() { + Log("Aborted!"); + reply->abort(); + if (data.onError_) { + data.onError_(-2); + } + }); + } - case DeleteRequest: { - reply = NetworkManager::NaM.deleteResource(data.request); - } break; - } + if (data.onReplyCreated_) { + data.onReplyCreated_(reply); + } - if (reply == nullptr) { - Log("Unhandled request type {}", (int)data.requestType); - return; - } + bool directAction = (data.caller_ == nullptr); - if (timer != nullptr) { - QObject::connect(timer, &QTimer::timeout, worker, - [reply, timer, data]() { - Log("Aborted!"); - reply->abort(); - timer->deleteLater(); - data.onError(-2); - }); - } + auto handleReply = [data = std::move(data), timer = std::move(timer), reply]() mutable { + if (reply->error() != QNetworkReply::NetworkError::NoError) { + if (data.onError_) { + data.onError_(reply->error()); + } + return; + } - if (data.onReplyCreated) { - data.onReplyCreated(reply); - } + QByteArray readBytes = reply->readAll(); + QByteArray bytes; + bytes.setRawData(readBytes.data(), readBytes.size()); + data.writeToCache(bytes); - QObject::connect(reply, &QNetworkReply::finished, worker, - [data = std::move(data), worker, reply]() mutable { - if (data.caller == nullptr) { - QByteArray bytes = reply->readAll(); - data.writeToCache(bytes); + NetworkResult result(bytes); + data.onSuccess_(result); - if (data.onSuccess) { - data.onSuccess(parseJSONFromData2(bytes)); - } else { - qWarning() << "data.onSuccess not found"; - } + reply->deleteLater(); + }; - reply->deleteLater(); - } else { - emit worker->doneUrl(reply); - } + if (data.caller_ != nullptr) { + QObject::connect(worker, &NetworkWorker::doneUrl, data.caller_, std::move(handleReply)); + QObject::connect(reply, &QNetworkReply::finished, worker, [worker]() mutable { + emit worker->doneUrl(); - delete worker; - }); - }); + delete worker; + }); + } else { + QObject::connect(reply, &QNetworkReply::finished, worker, + [handleReply = std::move(handleReply), worker]() mutable { + handleReply(); + + delete worker; + }); + } + }; + + QObject::connect(&requester, &NetworkRequester::requestUrl, worker, std::move(onUrlRequested)); emit requester.requestUrl(); } -void NetworkRequest::executeGet() +// Helper creator functions +NetworkRequest NetworkRequest::twitchRequest(QUrl url) { - this->useCache(); + NetworkRequest request(url); - this->doRequest(); + request.makeAuthorizedV5(getDefaultClientID()); + + return request; } -void NetworkRequest::executePut() -{ - this->doRequest(); -} - -void NetworkRequest::executeDelete() -{ - this->doRequest(); -} } // namespace chatterino diff --git a/src/common/NetworkRequest.hpp b/src/common/NetworkRequest.hpp index fc3b53859..3c83e8d86 100644 --- a/src/common/NetworkRequest.hpp +++ b/src/common/NetworkRequest.hpp @@ -1,248 +1,70 @@ #pragma once #include "Application.hpp" -#include "common/NetworkManager.hpp" +#include "common/NetworkCommon.hpp" +#include "common/NetworkData.hpp" #include "common/NetworkRequester.hpp" +#include "common/NetworkResult.hpp" +#include "common/NetworkTimer.hpp" #include "common/NetworkWorker.hpp" -#include "singletons/Paths.hpp" - -#include -#include -#include -#include namespace chatterino { -static QJsonObject parseJSONFromData(const QByteArray &data) -{ - QJsonDocument jsonDoc(QJsonDocument::fromJson(data)); - - if (jsonDoc.isNull()) { - return QJsonObject(); - } - - return jsonDoc.object(); -} - -static rapidjson::Document parseJSONFromData2(const QByteArray &data) -{ - rapidjson::Document ret(rapidjson::kNullType); - - rapidjson::ParseResult result = ret.Parse(data.data(), data.length()); - - if (result.Code() != rapidjson::kParseErrorNone) { - Log("JSON parse error: {} ({})", rapidjson::GetParseError_En(result.Code()), - result.Offset()); - return ret; - } - - return ret; -} - class NetworkRequest { -public: - enum RequestType { - GetRequest, - PostRequest, - PutRequest, - DeleteRequest, - }; + // Stores all data about the request that needs to be passed around to each part of the request + NetworkData data; -private: - struct Data { - QNetworkRequest request; - const QObject *caller = nullptr; - std::function onReplyCreated; - int timeoutMS = -1; - bool useQuickLoadCache = false; + // Timer that tracks the timeout + // By default, there's no explicit timeout for the request + // to enable the timer, the "setTimeout" function needs to be called before execute is called + std::unique_ptr timer; - std::function onError; - std::function onSuccess; - - NetworkRequest::RequestType requestType; - - QByteArray payload; - - QString getHash() - { - if (this->hash.isEmpty()) { - QByteArray bytes; - - bytes.append(this->request.url().toString()); - - for (const auto &header : this->request.rawHeaderList()) { - bytes.append(header); - } - - QByteArray hashBytes(QCryptographicHash::hash(bytes, QCryptographicHash::Sha256)); - - this->hash = hashBytes.toHex(); - } - - return this->hash; - } - - void writeToCache(const QByteArray &bytes); - - private: - QString hash; - } data; + // The NetworkRequest destructor will assert if executed_ hasn't been set to true before dying + bool executed_ = false; public: NetworkRequest() = delete; - explicit NetworkRequest(const char *url); - explicit NetworkRequest(const std::string &url); - explicit NetworkRequest(const QString &url); - NetworkRequest(QUrl url); + NetworkRequest(const NetworkRequest &other) = delete; + NetworkRequest &operator=(const NetworkRequest &other) = delete; - void setRequestType(RequestType newRequestType); + NetworkRequest(NetworkRequest &&other) = default; + NetworkRequest &operator=(NetworkRequest &&other) = default; - template - void onError(Func cb) - { - this->data.onError = cb; - } + explicit NetworkRequest(const std::string &url, + NetworkRequestType requestType = NetworkRequestType::Get); + NetworkRequest(QUrl url, NetworkRequestType requestType = NetworkRequestType::Get); - template - void onSuccess(Func cb) - { - this->data.onSuccess = cb; - } + ~NetworkRequest(); - void setPayload(const QByteArray &payload) - { - this->data.payload = payload; - } + void setRequestType(NetworkRequestType newRequestType); + void onReplyCreated(NetworkReplyCreatedCallback cb); + void onError(NetworkErrorCallback cb); + void onSuccess(NetworkSuccessCallback cb); + + void setPayload(const QByteArray &payload); void setUseQuickLoadCache(bool value); void setCaller(const QObject *caller); - void setOnReplyCreated(std::function f); void setRawHeader(const char *headerName, const char *value); void setRawHeader(const char *headerName, const QByteArray &value); void setRawHeader(const char *headerName, const QString &value); void setTimeout(int ms); void makeAuthorizedV5(const QString &clientID, const QString &oauthToken = QString()); - template - void get(FinishedCallback onFinished) - { - if (this->data.useQuickLoadCache) { - auto app = getApp(); - - QFile cachedFile(app->paths->cacheDirectory + "/" + this->data.getHash()); - - if (cachedFile.exists()) { - if (cachedFile.open(QIODevice::ReadOnly)) { - QByteArray bytes = cachedFile.readAll(); - - // qDebug() << "Loaded cached resource" << this->data.request.url(); - - bool success = onFinished(bytes); - - cachedFile.close(); - - if (!success) { - // The images were not successfully loaded from the file - // XXX: Invalidate the cache file so we don't attempt to load it again next - // time - } - } - } - } - - QTimer *timer = nullptr; - if (this->data.timeoutMS > 0) { - timer = new QTimer; - } - - NetworkRequester requester; - NetworkWorker *worker = new NetworkWorker; - - worker->moveToThread(&NetworkManager::workerThread); - - if (this->data.caller != nullptr) { - QObject::connect(worker, &NetworkWorker::doneUrl, this->data.caller, - [onFinished, data = this->data](auto reply) mutable { - if (reply->error() != QNetworkReply::NetworkError::NoError) { - if (data.onError) { - data.onError(reply->error()); - } - return; - } - - QByteArray readBytes = reply->readAll(); - QByteArray bytes; - bytes.setRawData(readBytes.data(), readBytes.size()); - data.writeToCache(bytes); - onFinished(bytes); - - reply->deleteLater(); - }); - } - - if (timer != nullptr) { - timer->start(this->data.timeoutMS); - } - - QObject::connect( - &requester, &NetworkRequester::requestUrl, worker, - [timer, data = std::move(this->data), worker, onFinished{std::move(onFinished)}]() { - QNetworkReply *reply = NetworkManager::NaM.get(data.request); - - if (timer != nullptr) { - QObject::connect(timer, &QTimer::timeout, worker, [reply, timer]() { - Log("Aborted!"); - reply->abort(); - timer->deleteLater(); - }); - } - - if (data.onReplyCreated) { - data.onReplyCreated(reply); - } - - QObject::connect(reply, &QNetworkReply::finished, worker, - [data = std::move(data), worker, reply, - onFinished = std::move(onFinished)]() mutable { - if (data.caller == nullptr) { - QByteArray bytes = reply->readAll(); - data.writeToCache(bytes); - onFinished(bytes); - - reply->deleteLater(); - } else { - emit worker->doneUrl(reply); - } - - delete worker; - }); - }); - - emit requester.requestUrl(); - } - - template - void getJSON(FinishedCallback onFinished) - { - this->get([onFinished{std::move(onFinished)}](const QByteArray &bytes) -> bool { - auto object = parseJSONFromData(bytes); - onFinished(object); - - // XXX: Maybe return onFinished? For now I don't want to force onFinished to have a - // return value - return true; - }); - } - void execute(); private: - void useCache(); + // Returns true if the file was successfully loaded from cache + // Returns false if the cache file either didn't exist, or it contained "invalid" data + // "invalid" is specified by the onSuccess callback + bool tryLoadCachedFile(); + void doRequest(); - void executeGet(); - void executePut(); - void executeDelete(); + +public: + // Helper creator functions + static NetworkRequest twitchRequest(QUrl url); }; } // namespace chatterino diff --git a/src/common/NetworkResult.cpp b/src/common/NetworkResult.cpp new file mode 100644 index 000000000..80eb582ff --- /dev/null +++ b/src/common/NetworkResult.cpp @@ -0,0 +1,46 @@ +#include "common/NetworkResult.hpp" + +#include "debug/Log.hpp" + +#include +#include +#include + +namespace chatterino { + +NetworkResult::NetworkResult(const QByteArray &data) + : data_(data) +{ +} + +QJsonObject NetworkResult::parseJson() const +{ + QJsonDocument jsonDoc(QJsonDocument::fromJson(this->data_)); + if (jsonDoc.isNull()) { + return QJsonObject{}; + } + + return jsonDoc.object(); +} + +rapidjson::Document NetworkResult::parseRapidJson() const +{ + rapidjson::Document ret(rapidjson::kNullType); + + rapidjson::ParseResult result = ret.Parse(this->data_.data(), this->data_.length()); + + if (result.Code() != rapidjson::kParseErrorNone) { + Log("JSON parse error: {} ({})", rapidjson::GetParseError_En(result.Code()), + result.Offset()); + return ret; + } + + return ret; +} + +QByteArray NetworkResult::getData() const +{ + return this->data_; +} + +} // namespace chatterino diff --git a/src/common/NetworkResult.hpp b/src/common/NetworkResult.hpp new file mode 100644 index 000000000..5631e1fe4 --- /dev/null +++ b/src/common/NetworkResult.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace chatterino { + +class NetworkResult +{ + QByteArray data_; + +public: + NetworkResult(const QByteArray &data); + + QJsonObject parseJson() const; + rapidjson::Document parseRapidJson() const; + QByteArray getData() const; +}; + +} // namespace chatterino diff --git a/src/common/NetworkTimer.hpp b/src/common/NetworkTimer.hpp new file mode 100644 index 000000000..64dcf0014 --- /dev/null +++ b/src/common/NetworkTimer.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "common/NetworkWorker.hpp" + +#include + +#include +#include +#include + +namespace chatterino { + +class NetworkTimer +{ + std::unique_ptr timer_; + + bool started_{}; + +public: + int timeoutMS_ = -1; + + NetworkTimer() = default; + ~NetworkTimer() = default; + + NetworkTimer(const NetworkTimer &other) = delete; + NetworkTimer &operator=(const NetworkTimer &other) = delete; + + NetworkTimer(NetworkTimer &&other) = default; + NetworkTimer &operator=(NetworkTimer &&other) = default; + + void start() + { + if (this->timeoutMS_ <= 0) { + return; + } + + this->timer_ = std::make_unique(); + this->timer_->start(this->timeoutMS_); + + this->started_ = true; + } + + bool isStarted() const + { + return this->started_; + } + + void onTimeout(NetworkWorker *worker, std::function cb) const + { + if (!this->timer_) { + return; + } + + QObject::connect(this->timer_.get(), &QTimer::timeout, worker, cb); + } +}; + +} // namespace chatterino diff --git a/src/common/NetworkWorker.hpp b/src/common/NetworkWorker.hpp index 560e5b4f5..b63bb15b4 100644 --- a/src/common/NetworkWorker.hpp +++ b/src/common/NetworkWorker.hpp @@ -2,8 +2,6 @@ #include -class QNetworkReply; - namespace chatterino { class NetworkWorker : public QObject @@ -11,7 +9,7 @@ class NetworkWorker : public QObject Q_OBJECT signals: - void doneUrl(QNetworkReply *); + void doneUrl(); }; } // namespace chatterino diff --git a/src/common/UrlFetch.hpp b/src/common/UrlFetch.hpp index 5ed00ada3..5e27fd3d7 100644 --- a/src/common/UrlFetch.hpp +++ b/src/common/UrlFetch.hpp @@ -1,147 +1,36 @@ #pragma once -#include "common/NetworkManager.hpp" #include "common/NetworkRequest.hpp" -#include "controllers/accounts/AccountController.hpp" -#include "debug/Log.hpp" -#include "providers/twitch/TwitchCommon.hpp" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include -#include - namespace chatterino { -static void twitchApiGet(QString url, const QObject *caller, - std::function successCallback) -{ - NetworkRequest req(url); - req.setCaller(caller); - req.setRawHeader("Client-ID", getDefaultClientID()); - req.setRawHeader("Accept", "application/vnd.twitchtv.v5+json"); +// Not sure if I like these, but I'm trying them out - req.getJSON([=](const QJsonObject &node) { - successCallback(node); // - }); -} - -static void twitchApiGet2(QString url, const QObject *caller, bool useQuickLoadCache, - std::function successCallback) +static NetworkRequest makeGetChannelRequest(const QString &channelId, + const QObject *caller = nullptr) { - NetworkRequest request(url); - request.setRequestType(NetworkRequest::GetRequest); + QString url("https://api.twitch.tv/kraken/channels/" + channelId); + + auto request = NetworkRequest::twitchRequest(url); + request.setCaller(caller); - request.makeAuthorizedV5(getDefaultClientID()); - request.setUseQuickLoadCache(useQuickLoadCache); - request.onSuccess([successCallback](const rapidjson::Document &document) { - successCallback(document); // - - return true; - }); - - request.execute(); + return request; } -static void twitchApiGetUserID(QString username, const QObject *caller, - std::function successCallback) +static NetworkRequest makeGetStreamRequest(const QString &channelId, + const QObject *caller = nullptr) { - twitchApiGet( - "https://api.twitch.tv/kraken/users?login=" + username, caller, - [=](const QJsonObject &root) { - if (!root.value("users").isArray()) { - Log("API Error while getting user id, users is not an array"); - return; - } + QString url("https://api.twitch.tv/kraken/streams/" + channelId); - auto users = root.value("users").toArray(); - if (users.size() != 1) { - Log("API Error while getting user id, users array size is not 1"); - return; - } - if (!users[0].isObject()) { - Log("API Error while getting user id, first user is not an object"); - return; - } - auto firstUser = users[0].toObject(); - auto id = firstUser.value("_id"); - if (!id.isString()) { - Log("API Error: while getting user id, first user object `_id` key is not a " - "string"); - return; - } - successCallback(id.toString()); - }); -} -static void twitchApiPut(QUrl url, std::function successCallback) -{ - NetworkRequest request(url); - request.setRequestType(NetworkRequest::PutRequest); - request.setCaller(QThread::currentThread()); + auto request = NetworkRequest::twitchRequest(url); - auto currentTwitchUser = getApp()->accounts->twitch.getCurrent(); - QByteArray oauthToken; - if (currentTwitchUser) { - oauthToken = currentTwitchUser->getOAuthToken().toUtf8(); - } else { - // XXX(pajlada): Bail out? - } + request.setCaller(caller); - request.makeAuthorizedV5(getDefaultClientID(), currentTwitchUser->getOAuthToken()); - - request.onSuccess([successCallback](const auto &document) { - if (!document.IsNull()) { - successCallback(document); - } - - return true; - }); - - request.execute(); -} - -static void twitchApiDelete(QUrl url, std::function successCallback) -{ - NetworkRequest request(url); - request.setRequestType(NetworkRequest::DeleteRequest); - request.setCaller(QThread::currentThread()); - - auto currentTwitchUser = getApp()->accounts->twitch.getCurrent(); - QByteArray oauthToken; - if (currentTwitchUser) { - oauthToken = currentTwitchUser->getOAuthToken().toUtf8(); - } else { - // XXX(pajlada): Bail out? - } - - request.makeAuthorizedV5(getDefaultClientID(), currentTwitchUser->getOAuthToken()); - - request.onError([successCallback](int code) { - if (code >= 200 && code <= 299) { - successCallback(); - } - - return true; - }); - - request.onSuccess([successCallback](const auto &document) { - successCallback(); - - return true; - }); - - request.execute(); + return request; } } // namespace chatterino diff --git a/src/messages/Image.cpp b/src/messages/Image.cpp index 2d20ef6d2..44b204329 100644 --- a/src/messages/Image.cpp +++ b/src/messages/Image.cpp @@ -66,7 +66,8 @@ void Image::loadImage() NetworkRequest req(this->getUrl()); req.setCaller(this); req.setUseQuickLoadCache(true); - req.get([this](QByteArray bytes) -> bool { + req.onSuccess([this](auto result) -> bool { + auto bytes = result.getData(); QByteArray copy = QByteArray::fromRawData(bytes.constData(), bytes.length()); QBuffer buffer(©); buffer.open(QIODevice::ReadOnly); @@ -156,6 +157,8 @@ void Image::loadImage() return true; }); + + req.execute(); } void Image::gifUpdateTimout() diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index 9138ccafe..1c1f757c0 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -21,11 +21,12 @@ void BTTVEmotes::loadGlobalEmotes() { QString url("https://api.betterttv.net/2/emotes"); - NetworkRequest req(url); - req.setCaller(QThread::currentThread()); - req.setTimeout(30000); - req.setUseQuickLoadCache(true); - req.getJSON([this](QJsonObject &root) { + NetworkRequest request(url); + request.setCaller(QThread::currentThread()); + request.setTimeout(30000); + request.setUseQuickLoadCache(true); + request.onSuccess([this](auto result) { + auto root = result.parseJson(); auto emotes = root.value("emotes").toArray(); QString urlTemplate = "https:" + root.value("urlTemplate").toString(); @@ -49,7 +50,11 @@ void BTTVEmotes::loadGlobalEmotes() } this->globalEmoteCodes = codes; + + return true; }); + + request.execute(); } void BTTVEmotes::loadChannelEmotes(const QString &channelName, std::weak_ptr _map) @@ -60,15 +65,16 @@ void BTTVEmotes::loadChannelEmotes(const QString &channelName, std::weak_ptrclear(); @@ -110,7 +116,11 @@ void BTTVEmotes::loadChannelEmotes(const QString &channelName, std::weak_ptrchannelEmoteCodes[channelName] = codes; + + return true; }); + + request.execute(); } } // namespace chatterino diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 8ff6c8ebf..9b7d470d0 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -46,11 +46,12 @@ void FFZEmotes::loadGlobalEmotes() { QString url("https://api.frankerfacez.com/v1/set/global"); - NetworkRequest req(url); - req.setCaller(QThread::currentThread()); - req.setTimeout(30000); - req.setUseQuickLoadCache(true); - req.getJSON([this](QJsonObject &root) { + NetworkRequest request(url); + request.setCaller(QThread::currentThread()); + request.setTimeout(30000); + request.setUseQuickLoadCache(true); + request.onSuccess([this](auto result) { + auto root = result.parseJson(); auto sets = root.value("sets").toObject(); std::vector codes; @@ -75,7 +76,11 @@ void FFZEmotes::loadGlobalEmotes() this->globalEmoteCodes = codes; } + + return true; }); + + request.execute(); } void FFZEmotes::loadChannelEmotes(const QString &channelName, std::weak_ptr _map) @@ -84,15 +89,16 @@ void FFZEmotes::loadChannelEmotes(const QString &channelName, std::weak_ptrclear(); @@ -128,7 +134,11 @@ void FFZEmotes::loadChannelEmotes(const QString &channelName, std::weak_ptrchannelEmoteCodes[channelName] = codes; } + + return true; }); + + request.execute(); } } // namespace chatterino diff --git a/src/providers/twitch/PartialTwitchUser.cpp b/src/providers/twitch/PartialTwitchUser.cpp new file mode 100644 index 000000000..2033e977d --- /dev/null +++ b/src/providers/twitch/PartialTwitchUser.cpp @@ -0,0 +1,70 @@ +#include "providers/twitch/PartialTwitchUser.hpp" + +#include "common/NetworkRequest.hpp" +#include "debug/Log.hpp" +#include "providers/twitch/TwitchCommon.hpp" + +#include + +namespace chatterino { + +PartialTwitchUser PartialTwitchUser::byName(const QString &username) +{ + PartialTwitchUser user; + user.username_ = username; + + return user; +} + +PartialTwitchUser PartialTwitchUser::byId(const QString &id) +{ + PartialTwitchUser user; + user.id_ = id; + + return user; +} + +void PartialTwitchUser::getId(std::function successCallback, const QObject *caller) +{ + assert(!this->username_.isEmpty()); + + if (caller == nullptr) { + caller = QThread::currentThread(); + } + + NetworkRequest request("https://api.twitch.tv/kraken/users?login=" + this->username_); + request.setCaller(caller); + request.makeAuthorizedV5(getDefaultClientID()); + + request.onSuccess([successCallback](auto result) { + auto root = result.parseJson(); + if (!root.value("users").isArray()) { + Log("API Error while getting user id, users is not an array"); + return false; + } + + auto users = root.value("users").toArray(); + if (users.size() != 1) { + Log("API Error while getting user id, users array size is not 1"); + return false; + } + if (!users[0].isObject()) { + Log("API Error while getting user id, first user is not an object"); + return false; + } + auto firstUser = users[0].toObject(); + auto id = firstUser.value("_id"); + if (!id.isString()) { + Log("API Error: while getting user id, first user object `_id` key is not a " + "string"); + return false; + } + successCallback(id.toString()); + + return true; + }); + + request.execute(); +} + +} // namespace chatterino diff --git a/src/providers/twitch/PartialTwitchUser.hpp b/src/providers/twitch/PartialTwitchUser.hpp new file mode 100644 index 000000000..a868f6fea --- /dev/null +++ b/src/providers/twitch/PartialTwitchUser.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +#include + +namespace chatterino { + +// Experimental class to test a method of calling APIs on twitch users +class PartialTwitchUser +{ + PartialTwitchUser() = default; + + QString username_; + QString id_; + +public: + static PartialTwitchUser byName(const QString &username); + static PartialTwitchUser byId(const QString &id); + + void getId(std::function successCallback, const QObject *caller = nullptr); +}; + +} // namespace chatterino diff --git a/src/providers/twitch/TwitchAccount.cpp b/src/providers/twitch/TwitchAccount.cpp index f49227444..0d1c1f736 100644 --- a/src/providers/twitch/TwitchAccount.cpp +++ b/src/providers/twitch/TwitchAccount.cpp @@ -3,6 +3,7 @@ #include "common/NetworkRequest.hpp" #include "common/UrlFetch.hpp" #include "debug/Log.hpp" +#include "providers/twitch/PartialTwitchUser.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "util/RapidjsonHelpers.hpp" @@ -76,10 +77,10 @@ void TwitchAccount::loadIgnores() QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + "/blocks"); NetworkRequest req(url); - req.setRequestType(NetworkRequest::GetRequest); req.setCaller(QThread::currentThread()); req.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); - req.onSuccess([=](const rapidjson::Document &document) { + req.onSuccess([=](auto result) { + auto document = result.parseRapidJson(); if (!document.IsObject()) { return false; } @@ -125,9 +126,11 @@ void TwitchAccount::loadIgnores() void TwitchAccount::ignore(const QString &targetName, std::function onFinished) { - twitchApiGetUserID(targetName, QThread::currentThread(), [=](QString targetUserID) { - this->ignoreByID(targetUserID, targetName, onFinished); // - }); + const auto onIdFetched = [this, targetName, onFinished](QString targetUserId) { + this->ignoreByID(targetUserId, targetName, onFinished); // + }; + + PartialTwitchUser::byName(this->userName_).getId(onIdFetched); } void TwitchAccount::ignoreByID(const QString &targetUserID, const QString &targetName, @@ -136,8 +139,7 @@ void TwitchAccount::ignoreByID(const QString &targetUserID, const QString &targe QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + "/blocks/" + targetUserID); - NetworkRequest req(url); - req.setRequestType(NetworkRequest::PutRequest); + NetworkRequest req(url, NetworkRequestType::Put); req.setCaller(QThread::currentThread()); req.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); @@ -148,7 +150,8 @@ void TwitchAccount::ignoreByID(const QString &targetUserID, const QString &targe return true; }); - req.onSuccess([=](const rapidjson::Document &document) { + req.onSuccess([=](auto result) { + auto document = result.parseRapidJson(); if (!document.IsObject()) { onFinished(IgnoreResult_Failed, "Bad JSON data while ignoring user " + targetName); return false; @@ -190,9 +193,11 @@ void TwitchAccount::ignoreByID(const QString &targetUserID, const QString &targe void TwitchAccount::unignore(const QString &targetName, std::function onFinished) { - twitchApiGetUserID(targetName, QThread::currentThread(), [=](QString targetUserID) { - this->unignoreByID(targetUserID, targetName, onFinished); // - }); + const auto onIdFetched = [this, targetName, onFinished](QString targetUserId) { + this->unignoreByID(targetUserId, targetName, onFinished); // + }; + + PartialTwitchUser::byName(this->userName_).getId(onIdFetched); } void TwitchAccount::unignoreByID( @@ -202,8 +207,7 @@ void TwitchAccount::unignoreByID( QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + "/blocks/" + targetUserID); - NetworkRequest req(url); - req.setRequestType(NetworkRequest::DeleteRequest); + NetworkRequest req(url, NetworkRequestType::Delete); req.setCaller(QThread::currentThread()); req.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); @@ -215,7 +219,8 @@ void TwitchAccount::unignoreByID( return true; }); - req.onSuccess([=](const rapidjson::Document &document) { + req.onSuccess([=](auto result) { + auto document = result.parseRapidJson(); TwitchUser ignoredUser; ignoredUser.id = targetUserID; { @@ -238,7 +243,6 @@ void TwitchAccount::checkFollow(const QString targetUserID, targetUserID); NetworkRequest req(url); - req.setRequestType(NetworkRequest::GetRequest); req.setCaller(QThread::currentThread()); req.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); @@ -252,7 +256,8 @@ void TwitchAccount::checkFollow(const QString targetUserID, return true; }); - req.onSuccess([=](const rapidjson::Document &document) { + req.onSuccess([=](auto result) { + auto document = result.parseRapidJson(); onFinished(FollowResult_Following); return true; }); @@ -260,6 +265,53 @@ void TwitchAccount::checkFollow(const QString targetUserID, req.execute(); } +void TwitchAccount::followUser(const QString userID, std::function successCallback) +{ + QUrl requestUrl("https://api.twitch.tv/kraken/users/" + this->getUserId() + + "/follows/channels/" + userID); + + NetworkRequest request(requestUrl, NetworkRequestType::Put); + request.setCaller(QThread::currentThread()); + + request.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); + + // TODO: Properly check result of follow request + request.onSuccess([successCallback](auto result) { + successCallback(); + + return true; + }); + + request.execute(); +} + +void TwitchAccount::unfollowUser(const QString userID, std::function successCallback) +{ + QUrl requestUrl("https://api.twitch.tv/kraken/users/" + this->getUserId() + + "/follows/channels/" + userID); + + NetworkRequest request(requestUrl, NetworkRequestType::Delete); + request.setCaller(QThread::currentThread()); + + request.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); + + request.onError([successCallback](int code) { + if (code >= 200 && code <= 299) { + successCallback(); + } + + return true; + }); + + request.onSuccess([successCallback](const auto &document) { + successCallback(); + + return true; + }); + + request.execute(); +} + std::set TwitchAccount::getIgnores() const { std::lock_guard lock(this->ignoresMutex_); @@ -282,7 +334,6 @@ void TwitchAccount::loadEmotes(std::function QString url("https://api.twitch.tv/kraken/users/" + this->getUserId() + "/emotes"); NetworkRequest req(url); - req.setRequestType(NetworkRequest::GetRequest); req.setCaller(QThread::currentThread()); req.makeAuthorizedV5(this->getOAuthClient(), this->getOAuthToken()); @@ -297,8 +348,8 @@ void TwitchAccount::loadEmotes(std::function return true; }); - req.onSuccess([=](const rapidjson::Document &document) { - cb(document); + req.onSuccess([=](auto result) { + cb(result.parseRapidJson()); return true; }); diff --git a/src/providers/twitch/TwitchAccount.hpp b/src/providers/twitch/TwitchAccount.hpp index a92087401..ee215209a 100644 --- a/src/providers/twitch/TwitchAccount.hpp +++ b/src/providers/twitch/TwitchAccount.hpp @@ -65,6 +65,8 @@ public: std::function onFinished); void checkFollow(const QString targetUserID, std::function onFinished); + void followUser(const QString userID, std::function successCallback); + void unfollowUser(const QString userID, std::function successCallback); std::set getIgnores() const; diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 688ac906d..61de45318 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -2,9 +2,11 @@ #include "common/Common.hpp" #include "common/UrlFetch.hpp" +#include "controllers/accounts/AccountController.hpp" #include "debug/Log.hpp" #include "messages/Message.hpp" #include "providers/twitch/PubsubClient.hpp" +#include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" @@ -82,8 +84,16 @@ TwitchChannel::TwitchChannel(const QString &channelName, Communi::IrcConnection } } - twitchApiGet("https://tmi.twitch.tv/group/user/" + this->name + "/chatters", - QThread::currentThread(), refreshChatters); + NetworkRequest request("https://tmi.twitch.tv/group/user/" + this->name + "/chatters"); + + request.setCaller(QThread::currentThread()); + request.onSuccess([refreshChatters](auto result) { + refreshChatters(result.parseJson()); // + + return true; + }); + + request.execute(); }; doRefreshChatters(); @@ -153,7 +163,7 @@ void TwitchChannel::sendMessage(const QString &message) // Do last message processing QString parsedMessage = app->emotes->emojis.replaceShortCodes(message); - parsedMessage.trim(); + parsedMessage = parsedMessage.trimmed(); if (parsedMessage.isEmpty()) { return; @@ -325,23 +335,26 @@ void TwitchChannel::refreshLiveStatus() std::weak_ptr weak = this->shared_from_this(); - twitchApiGet2(url, QThread::currentThread(), false, [weak](const rapidjson::Document &d) { + auto request = makeGetStreamRequest(this->roomID, QThread::currentThread()); + + request.onSuccess([weak](auto result) { + auto d = result.parseRapidJson(); ChannelPtr shared = weak.lock(); if (!shared) { - return; + return false; } TwitchChannel *channel = dynamic_cast(shared.get()); if (!d.IsObject()) { Log("[TwitchChannel:refreshLiveStatus] root is not an object"); - return; + return false; } if (!d.HasMember("stream")) { Log("[TwitchChannel:refreshLiveStatus] Missing stream in root"); - return; + return false; } const auto &stream = d["stream"]; @@ -349,21 +362,21 @@ void TwitchChannel::refreshLiveStatus() if (!stream.IsObject()) { // Stream is offline (stream is most likely null) channel->setLive(false); - return; + return false; } if (!stream.HasMember("viewers") || !stream.HasMember("game") || !stream.HasMember("channel") || !stream.HasMember("created_at")) { Log("[TwitchChannel:refreshLiveStatus] Missing members in stream"); channel->setLive(false); - return; + return false; } const rapidjson::Value &streamChannel = stream["channel"]; if (!streamChannel.IsObject() || !streamChannel.HasMember("status")) { Log("[TwitchChannel:refreshLiveStatus] Missing member \"status\" in channel"); - return; + return false; } // Stream is live @@ -400,7 +413,11 @@ void TwitchChannel::refreshLiveStatus() // Signal all listeners that the stream status has been updated channel->updateLiveInfo.invoke(); + + return true; }); + + request.execute(); } void TwitchChannel::startRefreshLiveStatusTimer(int intervalMS) @@ -423,13 +440,18 @@ void TwitchChannel::fetchRecentMessages() static QString genericURL = "https://tmi.twitch.tv/api/rooms/%1/recent_messages?client_id=" + getDefaultClientID(); + NetworkRequest request(genericURL.arg(this->roomID)); + request.makeAuthorizedV5(getDefaultClientID()); + request.setCaller(QThread::currentThread()); + std::weak_ptr weak = this->shared_from_this(); - twitchApiGet(genericURL.arg(roomID), QThread::currentThread(), [weak](QJsonObject obj) { + request.onSuccess([weak](auto result) { + auto obj = result.parseJson(); ChannelPtr shared = weak.lock(); if (!shared) { - return; + return false; } auto channel = dynamic_cast(shared.get()); @@ -439,7 +461,7 @@ void TwitchChannel::fetchRecentMessages() QJsonArray msgArray = obj.value("messages").toArray(); if (msgArray.empty()) { - return; + return false; } std::vector messages; @@ -455,8 +477,13 @@ void TwitchChannel::fetchRecentMessages() messages.push_back(builder.build()); } } + channel->addMessagesAtStart(messages); + + return true; }); + + request.execute(); } } // namespace chatterino diff --git a/src/providers/twitch/TwitchEmotes.cpp b/src/providers/twitch/TwitchEmotes.cpp index df1e2e076..d067f320e 100644 --- a/src/providers/twitch/TwitchEmotes.cpp +++ b/src/providers/twitch/TwitchEmotes.cpp @@ -45,39 +45,6 @@ QString cleanUpCode(const QString &dirtyEmoteCode) return cleanCode; } -void loadSetData(std::shared_ptr emoteSet) -{ - Log("Load twitch emote set data for {}", emoteSet->key); - NetworkRequest req("https://braize.pajlada.com/chatterino/twitchemotes/set/" + emoteSet->key + - "/"); - - req.setRequestType(NetworkRequest::GetRequest); - - req.onError([](int errorCode) -> bool { - Log("Emote sets on ERROR {}", errorCode); - return true; - }); - - req.onSuccess([emoteSet](const rapidjson::Document &root) -> bool { - Log("Emote sets on success"); - if (!root.IsObject()) { - return false; - } - - std::string emoteSetID; - QString channelName; - if (!rj::getSafe(root, "channel_name", channelName)) { - return false; - } - - emoteSet->channelName = channelName; - - return true; - }); - - req.execute(); -} - } // namespace TwitchEmotes::TwitchEmotes() @@ -165,7 +132,7 @@ void TwitchEmotes::refresh(const std::shared_ptr &user) emoteSet->key = emoteSetJSON.name.GetString(); - loadSetData(emoteSet); + this->loadSetData(emoteSet); for (const rapidjson::Value &emoteJSON : emoteSetJSON.value.GetArray()) { if (!emoteJSON.IsObject()) { @@ -221,18 +188,17 @@ void TwitchEmotes::loadSetData(std::shared_ptr emoteSet) return; } - Log("Load twitch emote set data for {}..", emoteSet->key); NetworkRequest req("https://braize.pajlada.com/chatterino/twitchemotes/set/" + emoteSet->key + "/"); - - req.setRequestType(NetworkRequest::GetRequest); + req.setUseQuickLoadCache(true); req.onError([](int errorCode) -> bool { - Log("Emote sets on ERROR {}", errorCode); + Log("Error code {} while loading emote set data", errorCode); return true; }); - req.onSuccess([emoteSet](const rapidjson::Document &root) -> bool { + req.onSuccess([emoteSet](auto result) -> bool { + auto root = result.parseRapidJson(); if (!root.IsObject()) { return false; } diff --git a/src/singletons/Resources.cpp b/src/singletons/Resources.cpp index 730985f0f..db734ce2a 100644 --- a/src/singletons/Resources.cpp +++ b/src/singletons/Resources.cpp @@ -327,7 +327,8 @@ void Resources::loadChannelData(const QString &roomID, bool bypassCache) NetworkRequest req(url); req.setCaller(QThread::currentThread()); - req.getJSON([this, roomID](QJsonObject &root) { + req.onSuccess([this, roomID](auto result) { + auto root = result.parseJson(); QJsonObject sets = root.value("badge_sets").toObject(); Resources::Channel &ch = this->channels[roomID]; @@ -348,49 +349,58 @@ void Resources::loadChannelData(const QString &roomID, bool bypassCache) } ch.loaded = true; + + return true; }); - QString cheermoteURL = "https://api.twitch.tv/kraken/bits/actions?channel_id=" + roomID; + req.execute(); - twitchApiGet2( - cheermoteURL, QThread::currentThread(), true, [this, roomID](const rapidjson::Document &d) { - Resources::Channel &ch = this->channels[roomID]; + QString cheermoteUrl = "https://api.twitch.tv/kraken/bits/actions?channel_id=" + roomID; + auto request = NetworkRequest::twitchRequest(cheermoteUrl); + request.setCaller(QThread::currentThread()); - ParseCheermoteSets(ch.jsonCheermoteSets, d); + request.onSuccess([this, roomID](auto result) { + auto d = result.parseRapidJson(); + Resources::Channel &ch = this->channels[roomID]; - for (auto &set : ch.jsonCheermoteSets) { - CheermoteSet cheermoteSet; - cheermoteSet.regex = - QRegularExpression("^" + set.prefix.toLower() + "([1-9][0-9]*)$"); + ParseCheermoteSets(ch.jsonCheermoteSets, d); - for (auto &tier : set.tiers) { - Cheermote cheermote; + for (auto &set : ch.jsonCheermoteSets) { + CheermoteSet cheermoteSet; + cheermoteSet.regex = QRegularExpression("^" + set.prefix.toLower() + "([1-9][0-9]*)$"); - cheermote.color = QColor(tier.color); - cheermote.minBits = tier.minBits; + for (auto &tier : set.tiers) { + Cheermote cheermote; - // TODO(pajlada): We currently hardcode dark here :| - // We will continue to do so for now since we haven't had to - // solve that anywhere else - cheermote.emoteDataAnimated.image1x = tier.images["dark"]["animated"]["1"]; - cheermote.emoteDataAnimated.image2x = tier.images["dark"]["animated"]["2"]; - cheermote.emoteDataAnimated.image3x = tier.images["dark"]["animated"]["4"]; + cheermote.color = QColor(tier.color); + cheermote.minBits = tier.minBits; - cheermote.emoteDataStatic.image1x = tier.images["dark"]["static"]["1"]; - cheermote.emoteDataStatic.image2x = tier.images["dark"]["static"]["2"]; - cheermote.emoteDataStatic.image3x = tier.images["dark"]["static"]["4"]; + // TODO(pajlada): We currently hardcode dark here :| + // We will continue to do so for now since we haven't had to + // solve that anywhere else + cheermote.emoteDataAnimated.image1x = tier.images["dark"]["animated"]["1"]; + cheermote.emoteDataAnimated.image2x = tier.images["dark"]["animated"]["2"]; + cheermote.emoteDataAnimated.image3x = tier.images["dark"]["animated"]["4"]; - cheermoteSet.cheermotes.emplace_back(cheermote); - } + cheermote.emoteDataStatic.image1x = tier.images["dark"]["static"]["1"]; + cheermote.emoteDataStatic.image2x = tier.images["dark"]["static"]["2"]; + cheermote.emoteDataStatic.image3x = tier.images["dark"]["static"]["4"]; - std::sort(cheermoteSet.cheermotes.begin(), cheermoteSet.cheermotes.end(), - [](const auto &lhs, const auto &rhs) { - return lhs.minBits < rhs.minBits; // - }); - - ch.cheermoteSets.emplace_back(cheermoteSet); + cheermoteSet.cheermotes.emplace_back(cheermote); } - }); + + std::sort(cheermoteSet.cheermotes.begin(), cheermoteSet.cheermotes.end(), + [](const auto &lhs, const auto &rhs) { + return lhs.minBits < rhs.minBits; // + }); + + ch.cheermoteSets.emplace_back(cheermoteSet); + } + + return true; + }); + + request.execute(); } void Resources::loadDynamicTwitchBadges() @@ -399,7 +409,8 @@ void Resources::loadDynamicTwitchBadges() NetworkRequest req(url); req.setCaller(QThread::currentThread()); - req.getJSON([this](QJsonObject &root) { + req.onSuccess([this](auto result) { + auto root = result.parseJson(); QJsonObject sets = root.value("badge_sets").toObject(); for (QJsonObject::iterator it = sets.begin(); it != sets.end(); ++it) { QJsonObject versions = it.value().toObject().value("versions").toObject(); @@ -417,7 +428,11 @@ void Resources::loadDynamicTwitchBadges() } this->dynamicBadgesLoaded = true; + + return true; }); + + req.execute(); } void Resources::loadChatterinoBadges() @@ -429,7 +444,8 @@ void Resources::loadChatterinoBadges() NetworkRequest req(url); req.setCaller(QThread::currentThread()); - req.getJSON([this](QJsonObject &root) { + req.onSuccess([this](auto result) { + auto root = result.parseJson(); QJsonArray badgeVariants = root.value("badges").toArray(); for (QJsonArray::iterator it = badgeVariants.begin(); it != badgeVariants.end(); ++it) { QJsonObject badgeVariant = it->toObject(); @@ -449,7 +465,11 @@ void Resources::loadChatterinoBadges() std::shared_ptr(badgeVariantPtr); } } + + return true; }); + + req.execute(); } } // namespace chatterino diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index c95809390..7de502c38 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -5,6 +5,7 @@ #include "util/CombinePath.hpp" #include "util/PostToThread.hpp" +#include #include #include @@ -94,7 +95,8 @@ void Updates::checkForUpdates() NetworkRequest req(url); req.setTimeout(30000); - req.getJSON([this](QJsonObject &object) { + req.onSuccess([this](auto result) { + auto object = result.parseJson(); QJsonValue version_val = object.value("version"); QJsonValue update_val = object.value("update"); diff --git a/src/widgets/dialogs/LoginDialog.cpp b/src/widgets/dialogs/LoginDialog.cpp index c652053b9..14fa58dcb 100644 --- a/src/widgets/dialogs/LoginDialog.cpp +++ b/src/widgets/dialogs/LoginDialog.cpp @@ -1,6 +1,10 @@ #include "widgets/dialogs/LoginDialog.hpp" + #include "common/Common.hpp" #include "common/UrlFetch.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "providers/twitch/PartialTwitchUser.hpp" + #ifdef USEWINSDK #include #endif @@ -173,9 +177,10 @@ AdvancedLoginWidget::AdvancedLoginWidget() this->ui_.buttonLowerRow.layout.addWidget(&this->ui_.buttonLowerRow.fillInUserIDButton); connect(&this->ui_.buttonLowerRow.fillInUserIDButton, &QPushButton::clicked, [=]() { - twitchApiGetUserID(this->ui_.usernameInput.text(), this, [=](const QString &userID) { + const auto onIdFetched = [=](const QString &userID) { this->ui_.userIDInput.setText(userID); // - }); + }; + PartialTwitchUser::byName(this->ui_.usernameInput.text()).getId(onIdFetched, this); }); } diff --git a/src/widgets/dialogs/LogsPopup.cpp b/src/widgets/dialogs/LogsPopup.cpp index 4c8b01349..a2afb9301 100644 --- a/src/widgets/dialogs/LogsPopup.cpp +++ b/src/widgets/dialogs/LogsPopup.cpp @@ -7,6 +7,7 @@ #include "widgets/helper/ChannelView.hpp" #include +#include #include #include @@ -65,8 +66,8 @@ void LogsPopup::getLogviewerLogs() return true; }); - req.getJSON([this, channelName](QJsonObject &data) { - + req.onSuccess([this, channelName](auto result) { + auto data = result.parseJson(); std::vector messages; ChannelPtr logsChannel(new Channel("logs", Channel::Type::None)); @@ -87,6 +88,8 @@ void LogsPopup::getLogviewerLogs() messages.push_back(builder.build()); }; this->setMessages(messages); + + return true; }); req.execute(); @@ -113,10 +116,12 @@ void LogsPopup::getOverrustleLogs() box->setAttribute(Qt::WA_DeleteOnClose); box->show(); box->raise(); + return true; }); - req.getJSON([this, channelName](QJsonObject &data) { + req.onSuccess([this, channelName](auto result) { + auto data = result.parseJson(); std::vector messages; if (data.contains("lines")) { QJsonArray dataMessages = data.value("lines").toArray(); @@ -135,7 +140,10 @@ void LogsPopup::getOverrustleLogs() } } this->setMessages(messages); + + return true; }); + req.execute(); } } // namespace chatterino diff --git a/src/widgets/dialogs/UserInfoPopup.cpp b/src/widgets/dialogs/UserInfoPopup.cpp index 9047c1c4f..02ae86703 100644 --- a/src/widgets/dialogs/UserInfoPopup.cpp +++ b/src/widgets/dialogs/UserInfoPopup.cpp @@ -2,6 +2,8 @@ #include "Application.hpp" #include "common/UrlFetch.hpp" +#include "controllers/accounts/AccountController.hpp" +#include "providers/twitch/PartialTwitchUser.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "singletons/Resources.hpp" #include "util/LayoutCreator.hpp" @@ -176,11 +178,15 @@ void UserInfoPopup::installEvents() QUrl requestUrl("https://api.twitch.tv/kraken/users/" + currentUser->getUserId() + "/follows/channels/" + this->userId_); + const auto reenableFollowCheckbox = [this] { + this->ui_.follow->setEnabled(true); // + }; + this->ui_.follow->setEnabled(false); if (this->ui_.follow->isChecked()) { - twitchApiPut(requestUrl, [this](const auto &) { this->ui_.follow->setEnabled(true); }); + currentUser->followUser(this->userId_, reenableFollowCheckbox); } else { - twitchApiDelete(requestUrl, [this] { this->ui_.follow->setEnabled(true); }); + currentUser->unfollowUser(this->userId_, reenableFollowCheckbox); } }); @@ -239,24 +245,28 @@ void UserInfoPopup::updateUserData() { std::weak_ptr hack = this->hack_; - // get user info - twitchApiGetUserID(this->userName_, this, [this, hack](QString id) { + const auto onIdFetched = [this, hack](QString id) { auto currentUser = getApp()->accounts->twitch.getCurrent(); this->userId_ = id; - // get channel info - twitchApiGet( - "https://api.twitch.tv/kraken/channels/" + id, this, [this](const QJsonObject &obj) { - this->ui_.followerCountLabel->setText( - TEXT_FOLLOWERS + QString::number(obj.value("followers").toInt())); - this->ui_.viewCountLabel->setText(TEXT_VIEWS + - QString::number(obj.value("views").toInt())); - this->ui_.createdDateLabel->setText( - TEXT_CREATED + obj.value("created_at").toString().section("T", 0, 0)); + auto request = makeGetChannelRequest(id, this); - this->loadAvatar(QUrl(obj.value("logo").toString())); - }); + request.onSuccess([this](auto result) { + auto obj = result.parseJson(); + this->ui_.followerCountLabel->setText(TEXT_FOLLOWERS + + QString::number(obj.value("followers").toInt())); + this->ui_.viewCountLabel->setText(TEXT_VIEWS + + QString::number(obj.value("views").toInt())); + this->ui_.createdDateLabel->setText( + TEXT_CREATED + obj.value("created_at").toString().section("T", 0, 0)); + + this->loadAvatar(QUrl(obj.value("logo").toString())); + + return true; + }); + + request.execute(); // get follow state currentUser->checkFollow(id, [this, hack](auto result) { @@ -279,7 +289,9 @@ void UserInfoPopup::updateUserData() this->ui_.ignore->setEnabled(true); this->ui_.ignore->setChecked(isIgnoring); - }); + }; + + PartialTwitchUser::byName(this->userName_).getId(onIdFetched, this); this->ui_.follow->setEnabled(false); this->ui_.ignore->setEnabled(false); @@ -386,16 +398,21 @@ UserInfoPopup::TimeoutWidget::TimeoutWidget() addTimeouts("sec", {{"1", 1}}); addTimeouts("min", { - {"1", 1 * 60}, {"5", 5 * 60}, {"10", 10 * 60}, + {"1", 1 * 60}, + {"5", 5 * 60}, + {"10", 10 * 60}, }); addTimeouts("hour", { - {"1", 1 * 60 * 60}, {"4", 4 * 60 * 60}, + {"1", 1 * 60 * 60}, + {"4", 4 * 60 * 60}, }); addTimeouts("days", { - {"1", 1 * 60 * 60 * 24}, {"3", 3 * 60 * 60 * 24}, + {"1", 1 * 60 * 60 * 24}, + {"3", 3 * 60 * 60 * 24}, }); addTimeouts("weeks", { - {"1", 1 * 60 * 60 * 24 * 7}, {"2", 2 * 60 * 60 * 24 * 7}, + {"1", 1 * 60 * 60 * 24 * 7}, + {"2", 2 * 60 * 60 * 24 * 7}, }); addButton(Ban, "ban", getApp()->resources->buttons.ban); diff --git a/src/widgets/splits/Split.cpp b/src/widgets/splits/Split.cpp index 2eaf132c2..a00cf5085 100644 --- a/src/widgets/splits/Split.cpp +++ b/src/widgets/splits/Split.cpp @@ -451,18 +451,25 @@ void Split::doOpenViewerList() } auto loadingLabel = new QLabel("Loading..."); - twitchApiGet("https://tmi.twitch.tv/group/user/" + this->getChannel()->name + "/chatters", this, - [=](QJsonObject obj) { - QJsonObject chattersObj = obj.value("chatters").toObject(); + auto request = NetworkRequest::twitchRequest("https://tmi.twitch.tv/group/user/" + + this->getChannel()->name + "/chatters"); - loadingLabel->hide(); - for (int i = 0; i < jsonLabels.size(); i++) { - chattersList->addItem(labelList.at(i)); - foreach (const QJsonValue &v, - chattersObj.value(jsonLabels.at(i)).toArray()) - chattersList->addItem(v.toString()); - } - }); + request.setCaller(this); + request.onSuccess([=](auto result) { + auto obj = result.parseJson(); + QJsonObject chattersObj = obj.value("chatters").toObject(); + + loadingLabel->hide(); + for (int i = 0; i < jsonLabels.size(); i++) { + chattersList->addItem(labelList.at(i)); + foreach (const QJsonValue &v, chattersObj.value(jsonLabels.at(i)).toArray()) + chattersList->addItem(v.toString()); + } + + return true; + }); + + request.execute(); searchBar->setPlaceholderText("Search User..."); QObject::connect(searchBar, &QLineEdit::textEdited, this, [=]() { diff --git a/src/widgets/splits/SplitHeader.cpp b/src/widgets/splits/SplitHeader.cpp index adfb318a9..23bb78fa4 100644 --- a/src/widgets/splits/SplitHeader.cpp +++ b/src/widgets/splits/SplitHeader.cpp @@ -2,6 +2,7 @@ #include "Application.hpp" #include "common/UrlFetch.hpp" +#include "controllers/accounts/AccountController.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchServer.hpp" #include "singletons/Resources.hpp" @@ -323,13 +324,13 @@ void SplitHeader::updateChannelText() if (streamStatus.live) { this->isLive_ = true; this->tooltip_ = "" - "

" + - streamStatus.title + "

" + streamStatus.game + "
" + - (streamStatus.rerun ? "Vod-casting" : "Live") + " for " + - streamStatus.uptime + " with " + - QString::number(streamStatus.viewerCount) + - " viewers" - "

"; + "

" + + streamStatus.title + "

" + streamStatus.game + "
" + + (streamStatus.rerun ? "Vod-casting" : "Live") + " for " + + streamStatus.uptime + " with " + + QString::number(streamStatus.viewerCount) + + " viewers" + "

"; if (streamStatus.rerun) { title += " (rerun)"; } else if (streamStatus.streamType.isEmpty()) { @@ -355,8 +356,8 @@ void SplitHeader::updateModerationModeIcon() auto app = getApp(); this->moderationButton_->setPixmap(this->split_->getModerationMode() - ? *app->resources->moderationmode_enabled->getPixmap() - : *app->resources->moderationmode_disabled->getPixmap()); + ? *app->resources->moderationmode_enabled->getPixmap() + : *app->resources->moderationmode_disabled->getPixmap()); bool modButtonVisible = false; ChannelPtr channel = this->split_->getChannel();