#include "providers/twitch/api/Helix.hpp" #include "common/Outcome.hpp" #include "common/QLogging.hpp" #include namespace chatterino { static Helix *instance = nullptr; void Helix::fetchUsers(QStringList userIds, QStringList userLogins, ResultCallback> 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 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 successCallback, HelixFailureCallback failureCallback) { QStringList userIds; QStringList userLogins{std::move(userName)}; this->fetchUsers( userIds, userLogins, [successCallback, failureCallback](const std::vector &users) { if (users.empty()) { failureCallback(); return; } successCallback(users[0]); }, failureCallback); } void Helix::getUserById(QString userId, ResultCallback 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 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 successCallback, HelixFailureCallback failureCallback) { this->fetchUsersFollows("", std::move(userId), std::move(successCallback), std::move(failureCallback)); } void Helix::fetchStreams( QStringList userIds, QStringList userLogins, ResultCallback> 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 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 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 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> 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 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> 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 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 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::createClip(QString channelId, ResultCallback successCallback, std::function failureCallback, std::function 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 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 successCallback, std::function 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> 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 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 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 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 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 successCallback, std::function 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> 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 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(); } void Helix::getEmoteSetData(QString emoteSetId, ResultCallback successCallback, HelixFailureCallback failureCallback) { QUrlQuery urlQuery; urlQuery.addQueryItem("emote_set_id", emoteSetId); this->makeRequest("chat/emotes/set", urlQuery) .onSuccess([successCallback, failureCallback, emoteSetId](auto result) -> Outcome { QJsonObject root = result.parseJson(); auto data = root.value("data"); if (!data.isArray() || data.toArray().isEmpty()) { failureCallback(); return Failure; } HelixEmoteSetData emoteSetData(data.toArray()[0].toObject()); successCallback(emoteSetData); return Success; }) .onError([failureCallback](NetworkResult result) { // TODO: make better xd failureCallback(); }) .execute(); } void Helix::getChannelEmotes( QString broadcasterId, ResultCallback> successCallback, HelixFailureCallback failureCallback) { QUrlQuery urlQuery; urlQuery.addQueryItem("broadcaster_id", broadcasterId); this->makeRequest("chat/emotes", urlQuery) .onSuccess([successCallback, failureCallback](NetworkResult result) -> Outcome { QJsonObject root = result.parseJson(); auto data = root.value("data"); if (!data.isArray()) { failureCallback(); return Failure; } std::vector channelEmotes; for (const auto &jsonStream : data.toArray()) { channelEmotes.emplace_back(jsonStream.toObject()); } successCallback(channelEmotes); return Success; }) .onError([failureCallback](auto result) { // TODO: make better xd 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