#include "providers/twitch/TwitchEmotes.hpp" #include "common/UrlFetch.hpp" #include "debug/Benchmark.hpp" #include "debug/Log.hpp" #include "messages/Image.hpp" #include "util/RapidjsonHelpers.hpp" #define TWITCH_EMOTE_TEMPLATE "https://static-cdn.jtvnw.net/emoticons/v1/{id}/{scale}" namespace chatterino { namespace { QString getEmoteLink(const QString &id, const QString &emoteScale) { QString value = TWITCH_EMOTE_TEMPLATE; value.detach(); return value.replace("{id}", id).replace("{scale}", emoteScale); } QString cleanUpCode(const QString &dirtyEmoteCode) { QString cleanCode = dirtyEmoteCode; // clang-format off static QMap emoteNameReplacements{ {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, {"\\<\\;3", "<3"}, {"\\:-?(o|O)", ":O"}, {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, {"\\:-?\\)", ":)"}, {"\\:-?D", ":D"}, {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, }; // clang-format on auto it = emoteNameReplacements.find(dirtyEmoteCode); if (it != emoteNameReplacements.end()) { cleanCode = it.value(); } cleanCode.replace("<", "<"); cleanCode.replace(">", ">"); 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() { { EmoteSet emoteSet; emoteSet.key = "19194"; emoteSet.text = "Twitch Prime Emotes"; this->staticEmoteSets[emoteSet.key] = std::move(emoteSet); } { EmoteSet emoteSet; emoteSet.key = "0"; emoteSet.text = "Twitch Global Emotes"; this->staticEmoteSets[emoteSet.key] = std::move(emoteSet); } } // id is used for lookup // emoteName is used for giving a name to the emote in case it doesn't exist EmoteData TwitchEmotes::getEmoteById(const QString &id, const QString &emoteName) { QString _emoteName = emoteName; _emoteName.replace("<", "<"); _emoteName.replace(">", ">"); // clang-format off static QMap emoteNameReplacements{ {"[oO](_|\\.)[oO]", "O_o"}, {"\\>\\;\\(", ">("}, {"\\<\\;3", "<3"}, {"\\:-?(o|O)", ":O"}, {"\\:-?(p|P)", ":P"}, {"\\:-?[\\\\/]", ":/"}, {"\\:-?[z|Z|\\|]", ":Z"}, {"\\:-?\\(", ":("}, {"\\:-?\\)", ":)"}, {"\\:-?D", ":D"}, {"\\;-?(p|P)", ";P"}, {"\\;-?\\)", ";)"}, {"R-?\\)", "R)"}, {"B-?\\)", "B)"}, }; // clang-format on auto it = emoteNameReplacements.find(_emoteName); if (it != emoteNameReplacements.end()) { _emoteName = it.value(); } return _twitchEmoteFromCache.getOrAdd(id, [&emoteName, &_emoteName, &id] { EmoteData newEmoteData; auto cleanCode = cleanUpCode(emoteName); newEmoteData.image1x = new chatterino::Image(getEmoteLink(id, "1.0"), 1, emoteName, _emoteName + "
Twitch Emote 1x"); newEmoteData.image1x->setCopyString(cleanCode); newEmoteData.image2x = new chatterino::Image(getEmoteLink(id, "2.0"), .5, emoteName, _emoteName + "
Twitch Emote 2x"); newEmoteData.image2x->setCopyString(cleanCode); newEmoteData.image3x = new chatterino::Image(getEmoteLink(id, "3.0"), .25, emoteName, _emoteName + "
Twitch Emote 3x"); newEmoteData.image3x->setCopyString(cleanCode); return newEmoteData; }); } void TwitchEmotes::refresh(const std::shared_ptr &user) { Log("Loading Twitch emotes for user {}", user->getUserName()); const auto &roomID = user->getUserId(); const auto &clientID = user->getOAuthClient(); const auto &oauthToken = user->getOAuthToken(); if (clientID.isEmpty() || oauthToken.isEmpty()) { Log("Missing Client ID or OAuth token"); return; } TwitchAccountEmoteData &emoteData = this->emotes[roomID]; if (emoteData.filled) { Log("Emotes are already loaded for room id {}", roomID); return; } QString url("https://api.twitch.tv/kraken/users/" + roomID + "/emotes"); auto loadEmotes = [=, &emoteData](const QJsonObject &root) { emoteData.emoteSets.clear(); emoteData.emoteCodes.clear(); auto emoticonSets = root.value("emoticon_sets").toObject(); for (QJsonObject::iterator it = emoticonSets.begin(); it != emoticonSets.end(); ++it) { auto emoteSet = std::make_shared(); emoteSet->key = it.key(); loadSetData(emoteSet); for (QJsonValue emoteValue : it.value().toArray()) { QJsonObject emoticon = emoteValue.toObject(); QString id = QString::number(emoticon["id"].toInt()); QString code = emoticon["code"].toString(); auto cleanCode = cleanUpCode(code); emoteSet->emotes.emplace_back(id, cleanCode); emoteData.emoteCodes.push_back(cleanCode); EmoteData emote = this->getEmoteById(id, code); emoteData.emotes.insert(code, emote); } emoteData.emoteSets.emplace_back(emoteSet); } emoteData.filled = true; }; twitchApiGetAuthorized(url, clientID, oauthToken, QThread::currentThread(), loadEmotes); } void TwitchEmotes::loadSetData(std::shared_ptr emoteSet) { if (!emoteSet) { Log("null emote set sent"); return; } auto staticSetIt = this->staticEmoteSets.find(emoteSet->key); if (staticSetIt != this->staticEmoteSets.end()) { const auto &staticSet = staticSetIt->second; emoteSet->channelName = staticSet.channelName; emoteSet->text = staticSet.text; 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.onError([](int errorCode) -> bool { Log("Emote sets on ERROR {}", errorCode); return true; }); req.onSuccess([emoteSet](const rapidjson::Document &root) -> bool { if (!root.IsObject()) { return false; } std::string emoteSetID; QString channelName; QString type; if (!rj::getSafe(root, "channel_name", channelName)) { return false; } if (!rj::getSafe(root, "type", type)) { return false; } Log("Loaded twitch emote set data for {}!", emoteSet->key); if (type == "sub") { emoteSet->text = QString("Twitch Subscriber Emote (%1)").arg(channelName); } else { emoteSet->text = QString("Twitch Account Emote (%1)").arg(channelName); } emoteSet->channelName = channelName; return true; }); req.execute(); } } // namespace chatterino