From 584e6e5643ade12e76fefa17e36e7a7b69c57ad5 Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Fri, 12 Jan 2018 19:37:11 +0100 Subject: [PATCH] Initial custom and channel-specific cheermote parsing done Needs more testing once the rendering pipeline is complete again Fixes #74 --- src/singletons/resourcemanager.cpp | 297 +++++++++++++++++++++++++++- src/singletons/resourcemanager.hpp | 45 ++++- src/twitch/twitchmessagebuilder.cpp | 95 +++++---- src/twitch/twitchmessagebuilder.hpp | 1 + 4 files changed, 390 insertions(+), 48 deletions(-) diff --git a/src/singletons/resourcemanager.cpp b/src/singletons/resourcemanager.cpp index 7fc745d81..305e66da4 100644 --- a/src/singletons/resourcemanager.cpp +++ b/src/singletons/resourcemanager.cpp @@ -1,6 +1,4 @@ #include "resourcemanager.hpp" -//#include "singletons/emotemanager.hpp" -//#include "singletons/windowmanager.hpp" #include "util/urlfetch.hpp" #include @@ -15,6 +13,257 @@ inline messages::Image *lli(const char *pixmapPath, qreal scale = 1) return new messages::Image(new QPixmap(pixmapPath), scale); } +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(ResourceManager::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()) { + ResourceManager::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 = new messages::Image(url, chatterinoScale); + + // TODO(pajlada): Fill in name and tooltip + tier.images[background][state][scale] = image; + } + } + } + + set.tiers.emplace_back(tier); + } + + return true; +} + +// 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 +inline void ParseCheermoteSets(std::vector &sets, + const rapidjson::Document &d) +{ + if (!d.IsObject()) { + return; + } + + if (!d.HasMember("actions")) { + return; + } + + const auto &actionsValue = d["actions"]; + + if (!actionsValue.IsArray()) { + return; + } + + for (const auto &action : actionsValue.GetArray()) { + ResourceManager::JSONCheermoteSet set; + bool res = ParseSingleCheermoteSet(set, action); + + if (res) { + sets.emplace_back(set); + } + } +} + } // namespace ResourceManager::ResourceManager() @@ -90,6 +339,48 @@ void ResourceManager::loadChannelData(const QString &roomID, bool bypassCache) ch.loaded = true; }); + + QString cheermoteURL = "https://api.twitch.tv/kraken/bits/actions?channel_id=" + roomID; + + util::twitch::get2( + cheermoteURL, QThread::currentThread(), [this, roomID](const rapidjson::Document &d) { + ResourceManager::Channel &ch = this->channels[roomID]; + + ParseCheermoteSets(ch.jsonCheermoteSets, d); + + for (auto &set : ch.jsonCheermoteSets) { + CheermoteSet cheermoteSet; + cheermoteSet.regex = + QRegularExpression("^" + set.prefix.toLower() + "([1-9][0-9]*)$"); + + for (auto &tier : set.tiers) { + Cheermote cheermote; + + cheermote.color = QColor(tier.color); + cheermote.minBits = tier.minBits; + + // 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.emoteDataStatic.image1x = tier.images["dark"]["static"]["1"]; + cheermote.emoteDataStatic.image2x = tier.images["dark"]["static"]["2"]; + cheermote.emoteDataStatic.image3x = tier.images["dark"]["static"]["4"]; + + 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); + } + }); } void ResourceManager::loadDynamicTwitchBadges() @@ -153,5 +444,5 @@ void ResourceManager::loadChatterinoBadges() }); } +} // namespace singletons } // namespace chatterino -} diff --git a/src/singletons/resourcemanager.hpp b/src/singletons/resourcemanager.hpp index 57bcc6792..7564a8f5b 100644 --- a/src/singletons/resourcemanager.hpp +++ b/src/singletons/resourcemanager.hpp @@ -1,6 +1,8 @@ #pragma once -#include "messages/image.hpp" +#include "util/emotemap.hpp" + +#include #include #include @@ -61,8 +63,47 @@ public: messages::Image *buttonBan; messages::Image *buttonTimeout; + 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; + }; + + struct Cheermote { + // a Cheermote indicates one tier + QColor color; + int minBits; + + util::EmoteData emoteDataAnimated; + util::EmoteData emoteDataStatic; + }; + + struct CheermoteSet { + QRegularExpression regex; + std::vector cheermotes; + }; + struct Channel { std::map badgeSets; + std::vector jsonCheermoteSets; + std::vector cheermoteSets; bool loaded = false; }; @@ -90,5 +131,5 @@ public: void loadChatterinoBadges(); }; +} // namespace singletons } // namespace chatterino -} diff --git a/src/twitch/twitchmessagebuilder.cpp b/src/twitch/twitchmessagebuilder.cpp index a729f0cf1..439e93ca6 100644 --- a/src/twitch/twitchmessagebuilder.cpp +++ b/src/twitch/twitchmessagebuilder.cpp @@ -80,9 +80,7 @@ MessagePtr TwitchMessageBuilder::parse() this->parseHighlights(); } - // bits - QString bits = ""; - + QString bits; auto iterator = this->tags.find("bits"); if (iterator != this->tags.end()) { bits = iterator.value().toString(); @@ -145,46 +143,8 @@ MessagePtr TwitchMessageBuilder::parse() if (!emoteData.isValid()) { // is text QString string = std::get<1>(tuple); - static QRegularExpression cheerRegex("cheer[1-9][0-9]*"); - - // cheers - if (!bits.isEmpty() && string.length() >= 6 && cheerRegex.match(string).isValid()) { - auto cheer = string.mid(5).toInt(); - - QString color; - - QColor bitsColor; - - if (cheer >= 10000) { - color = "red"; - bitsColor = QColor::fromHslF(0, 1, 0.5); - } else if (cheer >= 5000) { - color = "blue"; - bitsColor = QColor::fromHslF(0.61, 1, 0.4); - } else if (cheer >= 1000) { - color = "green"; - bitsColor = QColor::fromHslF(0.5, 1, 0.5); - } else if (cheer >= 100) { - color = "purple"; - bitsColor = QColor::fromHslF(0.8, 1, 0.5); - } else { - color = "gray"; - bitsColor = QColor::fromHslF(0.5f, 0.5f, 0.5f); - } - - QString bitsLinkAnimated = - QString("http://static-cdn.jtvnw.net/bits/dark/animated/" + color + "/1"); - QString bitsLink = - QString("http://static-cdn.jtvnw.net/bits/dark/static/" + color + "/1"); - - Image *imageAnimated = emoteManager.miscImageCache.getOrAdd( - bitsLinkAnimated, - [this, &bitsLinkAnimated] { return new Image(bitsLinkAnimated); }); - Image *image = emoteManager.miscImageCache.getOrAdd( - bitsLink, [this, &bitsLink] { return new Image(bitsLink); }); - - // append bits - + if (!bits.isEmpty() && this->tryParseCheermote(string)) { + // This string was parsed as a cheermote continue; } @@ -690,6 +650,55 @@ void TwitchMessageBuilder::addChatterinoBadges() ->setTooltip(QString::fromStdString(badge->tooltip)); } +bool TwitchMessageBuilder::tryParseCheermote(const QString &string) +{ + // Try to parse custom cheermotes + const auto &channelResources = + singletons::ResourceManager::getInstance().channels[this->roomID]; + if (channelResources.loaded) { + for (const auto &cheermoteSet : channelResources.cheermoteSets) { + auto match = cheermoteSet.regex.match(string); + if (!match.hasMatch()) { + continue; + } + QString amount = match.captured(1); + bool ok = false; + int numBits = amount.toInt(&ok); + if (!ok) { + debug::Log("Error parsing bit amount in tryParseCheermote"); + return false; + } + + auto savedIt = cheermoteSet.cheermotes.end(); + + // Fetch cheermote that matches our numBits + for (auto it = cheermoteSet.cheermotes.begin(); it != cheermoteSet.cheermotes.end(); + ++it) { + if (numBits >= it->minBits) { + savedIt = it; + } else { + break; + } + } + + if (savedIt == cheermoteSet.cheermotes.end()) { + debug::Log("Error getting a cheermote from a cheermote set for the bit amount {}", + numBits); + return false; + } + + const auto &cheermote = *savedIt; + + this->append(cheermote.emoteDataAnimated, EmoteElement::BitsAnimated); + this->append(amount, EmoteElement::Text, cheermote.color); + + return true; + } + } + + return false; +} + // bool // sortTwitchEmotes(const std::pair &a, // const std::pair &b) diff --git a/src/twitch/twitchmessagebuilder.hpp b/src/twitch/twitchmessagebuilder.hpp index 3cd42b754..a8800653a 100644 --- a/src/twitch/twitchmessagebuilder.hpp +++ b/src/twitch/twitchmessagebuilder.hpp @@ -63,6 +63,7 @@ private: void parseTwitchBadges(); void addChatterinoBadges(); + bool tryParseCheermote(const QString &string); }; } // namespace twitch