diff --git a/CHANGELOG.md b/CHANGELOG.md index da4043880..5cc0c9a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Bugfix: Fix Ctrl + Backspace not closing colon emote picker. (#2780) - Bugfix: Approving/denying AutoMod messages works again. (#2779) - Dev: Migrated AutoMod approve/deny endpoints to Helix. (#2779) +- Dev: Migrated Get Cheermotes endpoint to Helix. (#2440) ## 2.3.1 diff --git a/chatterino.pro b/chatterino.pro index 20e3f0e0f..2de1bda4b 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -220,7 +220,6 @@ SOURCES += \ src/providers/twitch/TwitchHelpers.cpp \ src/providers/twitch/TwitchIrcServer.cpp \ src/providers/twitch/TwitchMessageBuilder.cpp \ - src/providers/twitch/TwitchParseCheerEmotes.cpp \ src/providers/twitch/TwitchUser.cpp \ src/RunGui.cpp \ src/singletons/Badges.cpp \ @@ -459,7 +458,6 @@ HEADERS += \ src/providers/twitch/TwitchHelpers.hpp \ src/providers/twitch/TwitchIrcServer.hpp \ src/providers/twitch/TwitchMessageBuilder.hpp \ - src/providers/twitch/TwitchParseCheerEmotes.hpp \ src/providers/twitch/TwitchUser.hpp \ src/RunGui.hpp \ src/singletons/Badges.hpp \ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4c42203eb..04cbc19bf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -217,8 +217,6 @@ set(SOURCE_FILES main.cpp providers/twitch/TwitchIrcServer.hpp providers/twitch/TwitchMessageBuilder.cpp providers/twitch/TwitchMessageBuilder.hpp - providers/twitch/TwitchParseCheerEmotes.cpp - providers/twitch/TwitchParseCheerEmotes.hpp providers/twitch/TwitchUser.cpp providers/twitch/TwitchUser.hpp diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index b71151832..1f6037c8d 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -13,7 +13,6 @@ #include "providers/twitch/PubsubClient.hpp" #include "providers/twitch/TwitchCommon.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp" -#include "providers/twitch/TwitchParseCheerEmotes.hpp" #include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Kraken.hpp" #include "singletons/Emotes.hpp" @@ -910,28 +909,26 @@ void TwitchChannel::refreshBadges() void TwitchChannel::refreshCheerEmotes() { - QString url("https://api.twitch.tv/kraken/bits/actions?channel_id=" + - this->roomId()); - NetworkRequest::twitchRequest(url) - .onSuccess([this, - weak = weakOf(this)](auto result) -> Outcome { + getHelix()->getCheermotes( + this->roomId(), + [this, weak = weakOf(this)]( + const std::vector &cheermoteSets) -> Outcome { auto shared = weak.lock(); if (!shared) { return Failure; } - auto cheerEmoteSets = ParseCheermoteSets(result.parseRapidJson()); std::vector emoteSets; - for (auto &set : cheerEmoteSets) + for (const auto &set : cheermoteSets) { auto cheerEmoteSet = CheerEmoteSet(); cheerEmoteSet.regex = QRegularExpression( "^" + set.prefix + "([1-9][0-9]*)$", QRegularExpression::CaseInsensitiveOption); - for (auto &tier : set.tiers) + for (const auto &tier : set.tiers) { CheerEmote cheerEmote; @@ -949,36 +946,42 @@ void TwitchChannel::refreshCheerEmotes() cheerEmote.animatedEmote = std::make_shared( Emote{EmoteName{"cheer emote"}, ImageSet{ - tier.images["dark"]["animated"]["1"], - tier.images["dark"]["animated"]["2"], - tier.images["dark"]["animated"]["4"], + tier.darkAnimated.imageURL1x, + tier.darkAnimated.imageURL2x, + tier.darkAnimated.imageURL4x, }, Tooltip{emoteTooltip}, Url{}}); cheerEmote.staticEmote = std::make_shared( Emote{EmoteName{"cheer emote"}, ImageSet{ - tier.images["dark"]["static"]["1"], - tier.images["dark"]["static"]["2"], - tier.images["dark"]["static"]["4"], + tier.darkStatic.imageURL1x, + tier.darkStatic.imageURL2x, + tier.darkStatic.imageURL4x, }, Tooltip{emoteTooltip}, Url{}}); - cheerEmoteSet.cheerEmotes.emplace_back(cheerEmote); + cheerEmoteSet.cheerEmotes.emplace_back( + std::move(cheerEmote)); } + // Sort cheermotes by cost std::sort(cheerEmoteSet.cheerEmotes.begin(), cheerEmoteSet.cheerEmotes.end(), [](const auto &lhs, const auto &rhs) { return lhs.minBits > rhs.minBits; }); - emoteSets.emplace_back(cheerEmoteSet); + emoteSets.emplace_back(std::move(cheerEmoteSet)); } + *this->cheerEmoteSets_.access() = std::move(emoteSets); return Success; - }) - .execute(); + }, + [] { + // Failure + return Failure; + }); } void TwitchChannel::createClip() diff --git a/src/providers/twitch/TwitchParseCheerEmotes.cpp b/src/providers/twitch/TwitchParseCheerEmotes.cpp deleted file mode 100644 index 9667ffd2a..000000000 --- a/src/providers/twitch/TwitchParseCheerEmotes.cpp +++ /dev/null @@ -1,321 +0,0 @@ -#include "TwitchParseCheerEmotes.hpp" - -#include -#include -#include - -namespace chatterino { - -namespace { - - template - inline bool ReadValue(const rapidjson::Value &object, const char *key, - Type &out) - { - if (!object.HasMember(key)) - { - return false; - } - - const auto &value = object[key]; - - if (!value.Is()) - { - return false; - } - - out = value.Get(); - - return true; - } - - template <> - inline bool ReadValue(const rapidjson::Value &object, - const char *key, QString &out) - { - if (!object.HasMember(key)) - { - return false; - } - - const auto &value = object[key]; - - if (!value.IsString()) - { - return false; - } - - out = value.GetString(); - - return true; - } - - template <> - inline bool ReadValue>(const rapidjson::Value &object, - const char *key, - std::vector &out) - { - if (!object.HasMember(key)) - { - return false; - } - - const auto &value = object[key]; - - if (!value.IsArray()) - { - return false; - } - - for (const rapidjson::Value &innerValue : value.GetArray()) - { - if (!innerValue.IsString()) - { - return false; - } - - out.emplace_back(innerValue.GetString()); - } - - return true; - } - - // Parse a single cheermote set (or "action") from the twitch api - inline bool ParseSingleCheermoteSet(JSONCheermoteSet &set, - const rapidjson::Value &action) - { - if (!action.IsObject()) - { - return false; - } - - if (!ReadValue(action, "prefix", set.prefix)) - { - return false; - } - - if (!ReadValue(action, "scales", set.scales)) - { - return false; - } - - if (!ReadValue(action, "backgrounds", set.backgrounds)) - { - return false; - } - - if (!ReadValue(action, "states", set.states)) - { - return false; - } - - if (!ReadValue(action, "type", set.type)) - { - return false; - } - - if (!ReadValue(action, "updated_at", set.updatedAt)) - { - return false; - } - - if (!ReadValue(action, "priority", set.priority)) - { - return false; - } - - // Tiers - if (!action.HasMember("tiers")) - { - return false; - } - - const auto &tiersValue = action["tiers"]; - - if (!tiersValue.IsArray()) - { - return false; - } - - for (const rapidjson::Value &tierValue : tiersValue.GetArray()) - { - JSONCheermoteSet::CheermoteTier tier; - - if (!tierValue.IsObject()) - { - return false; - } - - if (!ReadValue(tierValue, "min_bits", tier.minBits)) - { - return false; - } - - if (!ReadValue(tierValue, "id", tier.id)) - { - return false; - } - - if (!ReadValue(tierValue, "color", tier.color)) - { - return false; - } - - // Images - if (!tierValue.HasMember("images")) - { - return false; - } - - const auto &imagesValue = tierValue["images"]; - - if (!imagesValue.IsObject()) - { - return false; - } - - // Read images object - for (const auto &imageBackgroundValue : imagesValue.GetObject()) - { - QString background = imageBackgroundValue.name.GetString(); - bool backgroundExists = false; - for (const auto &bg : set.backgrounds) - { - if (background == bg) - { - backgroundExists = true; - break; - } - } - - if (!backgroundExists) - { - continue; - } - - const rapidjson::Value &imageBackgroundStates = - imageBackgroundValue.value; - if (!imageBackgroundStates.IsObject()) - { - continue; - } - - // Read each key which represents a background - for (const auto &imageBackgroundState : - imageBackgroundStates.GetObject()) - { - QString state = imageBackgroundState.name.GetString(); - bool stateExists = false; - for (const auto &_state : set.states) - { - if (state == _state) - { - stateExists = true; - break; - } - } - - if (!stateExists) - { - continue; - } - - const rapidjson::Value &imageScalesValue = - imageBackgroundState.value; - if (!imageScalesValue.IsObject()) - { - continue; - } - - // Read each key which represents a scale - for (const auto &imageScaleValue : - imageScalesValue.GetObject()) - { - QString scale = imageScaleValue.name.GetString(); - bool scaleExists = false; - for (const auto &_scale : set.scales) - { - if (scale == _scale) - { - scaleExists = true; - break; - } - } - - if (!scaleExists) - { - continue; - } - - const rapidjson::Value &imageScaleURLValue = - imageScaleValue.value; - if (!imageScaleURLValue.IsString()) - { - continue; - } - - QString url = imageScaleURLValue.GetString(); - - bool ok = false; - qreal scaleNumber = scale.toFloat(&ok); - if (!ok) - { - continue; - } - - qreal chatterinoScale = 1 / scaleNumber; - - auto image = Image::fromUrl({url}, chatterinoScale); - - // TODO(pajlada): Fill in name and tooltip - tier.images[background][state][scale] = image; - } - } - } - - set.tiers.emplace_back(tier); - } - - return true; - } - -} // namespace - -// Look through the results of -// https://api.twitch.tv/kraken/bits/actions?channel_id=11148817 for cheermote -// sets or "Actions" as they are called in the API -std::vector ParseCheermoteSets(const rapidjson::Document &d) -{ - std::vector sets; - - if (!d.IsObject()) - { - return sets; - } - - if (!d.HasMember("actions")) - { - return sets; - } - - const auto &actionsValue = d["actions"]; - - if (!actionsValue.IsArray()) - { - return sets; - } - - for (const auto &action : actionsValue.GetArray()) - { - JSONCheermoteSet set; - bool res = ParseSingleCheermoteSet(set, action); - - if (res) - { - sets.emplace_back(set); - } - } - - return sets; -} -} // namespace chatterino diff --git a/src/providers/twitch/TwitchParseCheerEmotes.hpp b/src/providers/twitch/TwitchParseCheerEmotes.hpp deleted file mode 100644 index 284f87bf0..000000000 --- a/src/providers/twitch/TwitchParseCheerEmotes.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "messages/Image.hpp" - -namespace chatterino { - -struct JSONCheermoteSet { - QString prefix; - std::vector scales; - - std::vector backgrounds; - std::vector states; - - QString type; - QString updatedAt; - int priority; - - struct CheermoteTier { - int minBits; - QString id; - QString color; - - // Background State Scale - std::map>> - images; - }; - - std::vector tiers; -}; - -std::vector ParseCheermoteSets(const rapidjson::Document &d); - -} // namespace chatterino diff --git a/src/providers/twitch/api/Helix.cpp b/src/providers/twitch/api/Helix.cpp index 1f995313e..a105914a3 100644 --- a/src/providers/twitch/api/Helix.cpp +++ b/src/providers/twitch/api/Helix.cpp @@ -722,6 +722,45 @@ void Helix::manageAutoModMessages( .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(); +} + NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery) { assert(!url.startsWith("/")); diff --git a/src/providers/twitch/api/Helix.hpp b/src/providers/twitch/api/Helix.hpp index b9b53e16d..6e0273918 100644 --- a/src/providers/twitch/api/Helix.hpp +++ b/src/providers/twitch/api/Helix.hpp @@ -1,5 +1,6 @@ #pragma once +#include "common/Aliases.hpp" #include "common/NetworkRequest.hpp" #include @@ -193,6 +194,76 @@ struct HelixBlock { } }; +struct HelixCheermoteImage { + Url imageURL1x; + Url imageURL2x; + Url imageURL4x; + + explicit HelixCheermoteImage(QJsonObject jsonObject) + : imageURL1x(Url{jsonObject.value("1").toString()}) + , imageURL2x(Url{jsonObject.value("2").toString()}) + , imageURL4x(Url{jsonObject.value("4").toString()}) + { + } +}; + +struct HelixCheermoteTier { + QString id; + QString color; + int minBits; + HelixCheermoteImage darkAnimated; + HelixCheermoteImage darkStatic; + HelixCheermoteImage lightAnimated; + HelixCheermoteImage lightStatic; + + explicit HelixCheermoteTier(QJsonObject jsonObject) + : id(jsonObject.value("id").toString()) + , color(jsonObject.value("color").toString()) + , minBits(jsonObject.value("min_bits").toInt()) + , darkAnimated(jsonObject.value("images") + .toObject() + .value("dark") + .toObject() + .value("animated") + .toObject()) + , darkStatic(jsonObject.value("images") + .toObject() + .value("dark") + .toObject() + .value("static") + .toObject()) + , lightAnimated(jsonObject.value("images") + .toObject() + .value("light") + .toObject() + .value("animated") + .toObject()) + , lightStatic(jsonObject.value("images") + .toObject() + .value("light") + .toObject() + .value("static") + .toObject()) + { + } +}; + +struct HelixCheermoteSet { + QString prefix; + QString type; + std::vector tiers; + + explicit HelixCheermoteSet(QJsonObject jsonObject) + : prefix(jsonObject.value("prefix").toString()) + , type(jsonObject.value("type").toString()) + { + for (const auto &tier : jsonObject.value("tiers").toArray()) + { + this->tiers.emplace_back(tier.toObject()); + } + } +}; + enum class HelixClipError { Unknown, ClipsDisabled, @@ -321,6 +392,12 @@ public: std::function successCallback, std::function failureCallback); + // https://dev.twitch.tv/docs/api/reference/#get-cheermotes + void getCheermotes( + QString broadcasterId, + ResultCallback> successCallback, + HelixFailureCallback failureCallback); + void update(QString clientId, QString oauthToken); static void initialize(); diff --git a/src/providers/twitch/api/README.md b/src/providers/twitch/api/README.md index c279b39de..9b24e8f7e 100644 --- a/src/providers/twitch/api/README.md +++ b/src/providers/twitch/api/README.md @@ -6,14 +6,6 @@ this folder describes what sort of API requests we do, what permissions are requ We use few Kraken endpoints in Chatterino2. -### Get Cheermotes - -URL: https://dev.twitch.tv/docs/v5/reference/bits#get-cheermotes - -Migration path: **Not checked** - -- We implement this API in `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000` - ### Get User Emotes URL: https://dev.twitch.tv/docs/v5/reference/users#get-user-emotes @@ -88,16 +80,16 @@ Requires `clips:edit` scope URL: https://dev.twitch.tv/docs/api/reference#get-channel-information -- We implement this in `providers/twitch/api/Helix.cpp getChannel` +- We implement this in `providers/twitch/api/Helix.cpp getChannel` Used in: - `TwitchChannel` to refresh stream title ### Update Channel -URL: https://dev.twitch.tv/docs/api/reference#modify-channel-information +URL: https://dev.twitch.tv/docs/api/reference#modify-channel-information Requires `channel:manage:broadcast` scope -- We implement this in `providers/twitch/api/Helix.cpp updateChannel` +- We implement this in `providers/twitch/api/Helix.cpp updateChannel` Used in: - `/setgame` to update the game in the current channel - `/settitle` to update the title in the current channel @@ -113,29 +105,29 @@ Requires `user:edit:broadcast` scope ### Get User Block List -URL: https://dev.twitch.tv/docs/api/reference#get-user-block-list +URL: https://dev.twitch.tv/docs/api/reference#get-user-block-list Requires `user:read:blocked_users` scope -- We implement this in `providers/twitch/api/Helix.cpp loadBlocks` +- We implement this in `providers/twitch/api/Helix.cpp loadBlocks` Used in: - `providers/twitch/TwitchAccount.cpp loadBlocks` to load list of blocked (blocked) users by current user ### Block User -URL: https://dev.twitch.tv/docs/api/reference#block-user +URL: https://dev.twitch.tv/docs/api/reference#block-user Requires `user:manage:blocked_users` scope -- We implement this in `providers/twitch/api/Helix.cpp blockUser` +- We implement this in `providers/twitch/api/Helix.cpp blockUser` Used in: - `widgets/dialogs/UserInfoPopup.cpp` to block a user via checkbox in the usercard - `controllers/commands/CommandController.cpp` to block a user via "/block" command ### Unblock User -URL: https://dev.twitch.tv/docs/api/reference#unblock-user +URL: https://dev.twitch.tv/docs/api/reference#unblock-user Requires `user:manage:blocked_users` scope -- We implement this in `providers/twitch/api/Helix.cpp unblockUser` +- We implement this in `providers/twitch/api/Helix.cpp unblockUser` Used in: - `widgets/dialogs/UserInfoPopup.cpp` to unblock a user via checkbox in the usercard - `controllers/commands/CommandController.cpp` to unblock a user via "/unblock" command @@ -144,19 +136,27 @@ Requires `user:manage:blocked_users` scope URL: https://dev.twitch.tv/docs/api/reference#search-categories -- We implement this in `providers/twitch/api/Helix.cpp searchGames` +- We implement this in `providers/twitch/api/Helix.cpp searchGames` Used in: - `controllers/commands/CommandController.cpp` in `/setgame` command to fuzzy search for game titles ### Manage Held AutoMod Messages -URL: https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages +URL: https://dev.twitch.tv/docs/api/reference#manage-held-automod-messages Requires `moderator:manage:automod` scope -- We implement this in `providers/twitch/api/Helix.cpp manageAutoModMessages` +- We implement this in `providers/twitch/api/Helix.cpp manageAutoModMessages` Used in: - `providers/twitch/TwitchAccount.cpp` to approve/deny held AutoMod messages +### Get Cheermotes + +URL: https://dev.twitch.tv/docs/api/reference/#get-cheermotes + +- We implement this in `providers/twitch/api/Helix.cpp getCheermotes` + Used in: + - `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000` + ## TMI The TMI api is undocumented.