mirror-chatterino2/src/providers/twitch/api/Helix.cpp

816 lines
24 KiB
C++
Raw Normal View History

#include "providers/twitch/api/Helix.hpp"
#include "common/Outcome.hpp"
#include "common/QLogging.hpp"
#include <QJsonDocument>
namespace chatterino {
static Helix *instance = nullptr;
void Helix::fetchUsers(QStringList userIds, QStringList userLogins,
ResultCallback<std::vector<HelixUser>> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
for (const auto &id : userIds)
{
urlQuery.addQueryItem("id", id);
}
for (const auto &login : userLogins)
{
urlQuery.addQueryItem("login", login);
}
// TODO: set on success and on error
this->makeRequest("users", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixUser> users;
for (const auto &jsonUser : data.toArray())
{
users.emplace_back(jsonUser.toObject());
}
successCallback(users);
return Success;
})
.onError([failureCallback](auto /*result*/) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::getUserByName(QString userName,
ResultCallback<HelixUser> successCallback,
HelixFailureCallback failureCallback)
{
QStringList userIds;
QStringList userLogins{std::move(userName)};
this->fetchUsers(
userIds, userLogins,
[successCallback,
failureCallback](const std::vector<HelixUser> &users) {
if (users.empty())
{
failureCallback();
return;
}
successCallback(users[0]);
},
failureCallback);
}
void Helix::getUserById(QString userId,
ResultCallback<HelixUser> successCallback,
HelixFailureCallback failureCallback)
{
QStringList userIds{std::move(userId)};
QStringList userLogins;
this->fetchUsers(
userIds, userLogins,
[successCallback, failureCallback](const auto &users) {
if (users.empty())
{
failureCallback();
return;
}
successCallback(users[0]);
},
failureCallback);
}
void Helix::fetchUsersFollows(
QString fromId, QString toId,
ResultCallback<HelixUsersFollowsResponse> successCallback,
HelixFailureCallback failureCallback)
{
assert(!fromId.isEmpty() || !toId.isEmpty());
QUrlQuery urlQuery;
if (!fromId.isEmpty())
{
urlQuery.addQueryItem("from_id", fromId);
}
if (!toId.isEmpty())
{
urlQuery.addQueryItem("to_id", toId);
}
// TODO: set on success and on error
this->makeRequest("users/follows", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
if (root.empty())
{
failureCallback();
return Failure;
}
successCallback(HelixUsersFollowsResponse(root));
return Success;
})
.onError([failureCallback](auto /*result*/) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::getUserFollowers(
QString userId, ResultCallback<HelixUsersFollowsResponse> successCallback,
HelixFailureCallback failureCallback)
{
this->fetchUsersFollows("", std::move(userId), std::move(successCallback),
std::move(failureCallback));
}
void Helix::getUserFollow(
QString userId, QString targetId,
ResultCallback<bool, HelixUsersFollowsRecord> successCallback,
HelixFailureCallback failureCallback)
{
this->fetchUsersFollows(
std::move(userId), std::move(targetId),
[successCallback](const auto &response) {
if (response.data.empty())
{
successCallback(false, HelixUsersFollowsRecord());
return;
}
successCallback(true, response.data[0]);
},
std::move(failureCallback));
}
void Helix::fetchStreams(
QStringList userIds, QStringList userLogins,
ResultCallback<std::vector<HelixStream>> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
for (const auto &id : userIds)
{
urlQuery.addQueryItem("user_id", id);
}
for (const auto &login : userLogins)
{
urlQuery.addQueryItem("user_login", login);
}
// TODO: set on success and on error
this->makeRequest("streams", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixStream> streams;
for (const auto &jsonStream : data.toArray())
{
streams.emplace_back(jsonStream.toObject());
}
successCallback(streams);
return Success;
})
.onError([failureCallback](auto /*result*/) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::getStreamById(QString userId,
ResultCallback<bool, HelixStream> successCallback,
HelixFailureCallback failureCallback)
{
QStringList userIds{std::move(userId)};
QStringList userLogins;
this->fetchStreams(
userIds, userLogins,
[successCallback, failureCallback](const auto &streams) {
if (streams.empty())
{
successCallback(false, HelixStream());
return;
}
successCallback(true, streams[0]);
},
failureCallback);
}
void Helix::getStreamByName(QString userName,
ResultCallback<bool, HelixStream> successCallback,
HelixFailureCallback failureCallback)
{
QStringList userIds;
QStringList userLogins{std::move(userName)};
this->fetchStreams(
userIds, userLogins,
[successCallback, failureCallback](const auto &streams) {
if (streams.empty())
{
successCallback(false, HelixStream());
return;
}
successCallback(true, streams[0]);
},
failureCallback);
}
///
void Helix::fetchGames(QStringList gameIds, QStringList gameNames,
ResultCallback<std::vector<HelixGame>> successCallback,
HelixFailureCallback failureCallback)
{
assert((gameIds.length() + gameNames.length()) > 0);
QUrlQuery urlQuery;
for (const auto &id : gameIds)
{
urlQuery.addQueryItem("id", id);
}
for (const auto &login : gameNames)
{
urlQuery.addQueryItem("name", login);
}
// TODO: set on success and on error
this->makeRequest("games", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixGame> games;
for (const auto &jsonStream : data.toArray())
{
games.emplace_back(jsonStream.toObject());
}
successCallback(games);
return Success;
})
.onError([failureCallback](auto /*result*/) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::searchGames(QString gameName,
ResultCallback<std::vector<HelixGame>> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
urlQuery.addQueryItem("query", gameName);
this->makeRequest("search/categories", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixGame> games;
for (const auto &jsonStream : data.toArray())
{
games.emplace_back(jsonStream.toObject());
}
successCallback(games);
return Success;
})
.onError([failureCallback](auto /*result*/) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::getGameById(QString gameId,
ResultCallback<HelixGame> successCallback,
HelixFailureCallback failureCallback)
{
QStringList gameIds{std::move(gameId)};
QStringList gameNames;
this->fetchGames(
gameIds, gameNames,
[successCallback, failureCallback](const auto &games) {
if (games.empty())
{
failureCallback();
return;
}
successCallback(games[0]);
},
failureCallback);
}
void Helix::followUser(QString userId, QString targetId,
std::function<void()> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
urlQuery.addQueryItem("from_id", userId);
urlQuery.addQueryItem("to_id", targetId);
this->makeRequest("users/follows", urlQuery)
.type(NetworkRequestType::Post)
.onSuccess([successCallback](auto /*result*/) -> Outcome {
successCallback();
return Success;
})
.onError([failureCallback](auto /*result*/) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::unfollowUser(QString userId, QString targetId,
std::function<void()> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
urlQuery.addQueryItem("from_id", userId);
urlQuery.addQueryItem("to_id", targetId);
this->makeRequest("users/follows", urlQuery)
.type(NetworkRequestType::Delete)
.onSuccess([successCallback](auto /*result*/) -> Outcome {
successCallback();
return Success;
})
.onError([failureCallback](auto /*result*/) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::createClip(QString channelId,
ResultCallback<HelixClip> successCallback,
std::function<void(HelixClipError)> failureCallback,
std::function<void()> finallyCallback)
{
QUrlQuery urlQuery;
urlQuery.addQueryItem("broadcaster_id", channelId);
this->makeRequest("clips", urlQuery)
.type(NetworkRequestType::Post)
.header("Content-Type", "application/json")
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback(HelixClipError::Unknown);
return Failure;
}
HelixClip clip(data.toArray()[0].toObject());
successCallback(clip);
return Success;
})
.onError([failureCallback](auto result) {
switch (result.status())
{
case 503: {
// Channel has disabled clip-creation, or channel has made cliops only creatable by followers and the user is not a follower (or subscriber)
failureCallback(HelixClipError::ClipsDisabled);
}
break;
case 401: {
// User does not have the required scope to be able to create clips, user must reauthenticate
failureCallback(HelixClipError::UserNotAuthenticated);
}
break;
default: {
qCDebug(chatterinoTwitch)
<< "Failed to create a clip: " << result.status()
<< result.getData();
failureCallback(HelixClipError::Unknown);
}
break;
}
})
.finally(std::move(finallyCallback))
.execute();
}
void Helix::getChannel(QString broadcasterId,
ResultCallback<HelixChannel> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
urlQuery.addQueryItem("broadcaster_id", broadcasterId);
this->makeRequest("channels", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
HelixChannel channel(data.toArray()[0].toObject());
successCallback(channel);
return Success;
})
.onError([failureCallback](auto /*result*/) {
failureCallback();
})
.execute();
}
void Helix::createStreamMarker(
QString broadcasterId, QString description,
ResultCallback<HelixStreamMarker> successCallback,
std::function<void(HelixStreamMarkerError)> failureCallback)
{
QJsonObject payload;
if (!description.isEmpty())
{
payload.insert("description", QJsonValue(description));
}
payload.insert("user_id", QJsonValue(broadcasterId));
this->makeRequest("streams/markers", QUrlQuery())
.type(NetworkRequestType::Post)
.header("Content-Type", "application/json")
.payload(QJsonDocument(payload).toJson(QJsonDocument::Compact))
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback(HelixStreamMarkerError::Unknown);
return Failure;
}
HelixStreamMarker streamMarker(data.toArray()[0].toObject());
successCallback(streamMarker);
return Success;
})
.onError([failureCallback](NetworkResult result) {
switch (result.status())
{
case 403: {
// User isn't a Channel Editor, so he can't create markers
failureCallback(HelixStreamMarkerError::UserNotAuthorized);
}
break;
case 401: {
// User does not have the required scope to be able to create stream markers, user must reauthenticate
failureCallback(
HelixStreamMarkerError::UserNotAuthenticated);
}
break;
default: {
qCDebug(chatterinoTwitch)
<< "Failed to create a stream marker: "
<< result.status() << result.getData();
failureCallback(HelixStreamMarkerError::Unknown);
}
break;
}
})
.execute();
};
void Helix::loadBlocks(QString userId,
ResultCallback<std::vector<HelixBlock>> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
urlQuery.addQueryItem("broadcaster_id", userId);
urlQuery.addQueryItem("first", "100");
this->makeRequest("users/blocks", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixBlock> ignores;
for (const auto &jsonStream : data.toArray())
{
ignores.emplace_back(jsonStream.toObject());
}
successCallback(ignores);
return Success;
})
.onError([failureCallback](auto /*result*/) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::blockUser(QString targetUserId,
std::function<void()> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
urlQuery.addQueryItem("target_user_id", targetUserId);
this->makeRequest("users/blocks", urlQuery)
.type(NetworkRequestType::Put)
.onSuccess([successCallback](auto /*result*/) -> Outcome {
successCallback();
return Success;
})
.onError([failureCallback](auto /*result*/) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::unblockUser(QString targetUserId,
std::function<void()> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
urlQuery.addQueryItem("target_user_id", targetUserId);
this->makeRequest("users/blocks", urlQuery)
.type(NetworkRequestType::Delete)
.onSuccess([successCallback](auto /*result*/) -> Outcome {
successCallback();
return Success;
})
.onError([failureCallback](auto /*result*/) {
// TODO: make better xd
failureCallback();
})
.execute();
}
void Helix::updateChannel(QString broadcasterId, QString gameId,
QString language, QString title,
std::function<void(NetworkResult)> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
auto data = QJsonDocument();
auto obj = QJsonObject();
if (!gameId.isEmpty())
{
obj.insert("game_id", gameId);
}
if (!language.isEmpty())
{
obj.insert("broadcaster_language", language);
}
if (!title.isEmpty())
{
obj.insert("title", title);
}
if (title.isEmpty() && gameId.isEmpty() && language.isEmpty())
{
qCDebug(chatterinoCommon) << "Tried to update channel with no changes!";
return;
}
data.setObject(obj);
urlQuery.addQueryItem("broadcaster_id", broadcasterId);
this->makeRequest("channels", urlQuery)
.type(NetworkRequestType::Patch)
.header("Content-Type", "application/json")
.payload(data.toJson())
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
successCallback(result);
return Success;
})
.onError([failureCallback](NetworkResult result) {
failureCallback();
})
.execute();
}
void Helix::manageAutoModMessages(
QString userID, QString msgID, QString action,
std::function<void()> successCallback,
std::function<void(HelixAutoModMessageError)> failureCallback)
{
QJsonObject payload;
payload.insert("user_id", userID);
payload.insert("msg_id", msgID);
payload.insert("action", action);
this->makeRequest("moderation/automod/message", QUrlQuery())
.type(NetworkRequestType::Post)
.header("Content-Type", "application/json")
.payload(QJsonDocument(payload).toJson(QJsonDocument::Compact))
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
successCallback();
return Success;
})
.onError([failureCallback, msgID, action](NetworkResult result) {
switch (result.status())
{
case 400: {
// Message was already processed
failureCallback(
HelixAutoModMessageError::MessageAlreadyProcessed);
}
break;
case 401: {
// User is missing the required scope
failureCallback(
HelixAutoModMessageError::UserNotAuthenticated);
}
break;
case 403: {
// Requesting user is not authorized to manage messages
failureCallback(
HelixAutoModMessageError::UserNotAuthorized);
}
break;
case 404: {
// Message not found or invalid msgID
failureCallback(HelixAutoModMessageError::MessageNotFound);
}
break;
default: {
qCDebug(chatterinoTwitch)
<< "Failed to manage automod message: " << action
<< msgID << result.status() << result.getData();
failureCallback(HelixAutoModMessageError::Unknown);
}
break;
}
})
.execute();
}
void Helix::getCheermotes(
QString broadcasterId,
ResultCallback<std::vector<HelixCheermoteSet>> successCallback,
HelixFailureCallback failureCallback)
{
QUrlQuery urlQuery;
urlQuery.addQueryItem("broadcaster_id", broadcasterId);
this->makeRequest("bits/cheermotes", urlQuery)
.onSuccess([successCallback, failureCallback](auto result) -> Outcome {
auto root = result.parseJson();
auto data = root.value("data");
if (!data.isArray())
{
failureCallback();
return Failure;
}
std::vector<HelixCheermoteSet> cheermoteSets;
for (const auto &jsonStream : data.toArray())
{
cheermoteSets.emplace_back(jsonStream.toObject());
}
successCallback(cheermoteSets);
return Success;
})
.onError([broadcasterId, failureCallback](NetworkResult result) {
qCDebug(chatterinoTwitch)
<< "Failed to get cheermotes(broadcaster_id=" << broadcasterId
<< "): " << result.status() << result.getData();
failureCallback();
})
.execute();
}
NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery)
{
assert(!url.startsWith("/"));
if (this->clientId.isEmpty())
{
qCDebug(chatterinoTwitch)
<< "Helix::makeRequest called without a client ID set BabyRage";
// return boost::none;
}
if (this->oauthToken.isEmpty())
{
qCDebug(chatterinoTwitch)
<< "Helix::makeRequest called without an oauth token set BabyRage";
// return boost::none;
}
const QString baseUrl("https://api.twitch.tv/helix/");
QUrl fullUrl(baseUrl + url);
fullUrl.setQuery(urlQuery);
return NetworkRequest(fullUrl)
.timeout(5 * 1000)
.header("Accept", "application/json")
.header("Client-ID", this->clientId)
.header("Authorization", "Bearer " + this->oauthToken);
}
void Helix::update(QString clientId, QString oauthToken)
{
this->clientId = std::move(clientId);
this->oauthToken = std::move(oauthToken);
}
void Helix::initialize()
{
assert(instance == nullptr);
instance = new Helix();
}
Helix *getHelix()
{
assert(instance != nullptr);
return instance;
}
} // namespace chatterino