2018-06-05 17:13:29 +02:00
|
|
|
#include "providers/twitch/twitchemotes.hpp"
|
|
|
|
|
|
|
|
#include "debug/log.hpp"
|
|
|
|
#include "messages/image.hpp"
|
2018-06-24 14:42:40 +02:00
|
|
|
#include "util/benchmark.hpp"
|
|
|
|
#include "util/rapidjson-helpers.hpp"
|
2018-06-05 17:13:29 +02:00
|
|
|
#include "util/urlfetch.hpp"
|
|
|
|
|
|
|
|
#define TWITCH_EMOTE_TEMPLATE "https://static-cdn.jtvnw.net/emoticons/v1/{id}/{scale}"
|
|
|
|
|
|
|
|
namespace chatterino {
|
|
|
|
namespace providers {
|
|
|
|
namespace twitch {
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
2018-06-07 13:01:06 +02:00
|
|
|
QString getEmoteLink(const QString &id, const QString &emoteScale)
|
2018-06-05 17:13:29 +02:00
|
|
|
{
|
|
|
|
QString value = TWITCH_EMOTE_TEMPLATE;
|
|
|
|
|
|
|
|
value.detach();
|
|
|
|
|
2018-06-07 13:01:06 +02:00
|
|
|
return value.replace("{id}", id).replace("{scale}", emoteScale);
|
2018-06-05 17:13:29 +02:00
|
|
|
}
|
|
|
|
|
2018-06-19 22:04:12 +02:00
|
|
|
QString cleanUpCode(const QString &dirtyEmoteCode)
|
|
|
|
{
|
2018-06-24 18:35:38 +02:00
|
|
|
QString cleanCode = dirtyEmoteCode;
|
2018-06-19 22:04:12 +02:00
|
|
|
// clang-format off
|
|
|
|
static QMap<QString, QString> 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()) {
|
2018-06-24 18:35:38 +02:00
|
|
|
cleanCode = it.value();
|
2018-06-19 22:04:12 +02:00
|
|
|
}
|
|
|
|
|
2018-06-24 18:35:38 +02:00
|
|
|
cleanCode.replace("<", "<");
|
|
|
|
cleanCode.replace(">", ">");
|
|
|
|
|
|
|
|
return cleanCode;
|
2018-06-19 22:04:12 +02:00
|
|
|
}
|
|
|
|
|
2018-06-24 14:42:40 +02:00
|
|
|
void loadSetData(std::shared_ptr<TwitchEmotes::EmoteSet> emoteSet)
|
|
|
|
{
|
|
|
|
debug::Log("Load twitch emote set data for {}", emoteSet->key);
|
|
|
|
util::NetworkRequest req("https://braize.pajlada.com/chatterino/twitchemotes/set/" +
|
|
|
|
emoteSet->key + "/");
|
|
|
|
|
|
|
|
req.setRequestType(util::NetworkRequest::GetRequest);
|
|
|
|
|
|
|
|
req.onError([](int errorCode) -> bool {
|
|
|
|
debug::Log("Emote sets on ERROR {}", errorCode);
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
|
|
|
|
req.onSuccess([emoteSet](const rapidjson::Document &root) -> bool {
|
|
|
|
debug::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();
|
|
|
|
}
|
|
|
|
|
2018-06-05 17:13:29 +02:00
|
|
|
} // namespace
|
|
|
|
|
2018-06-24 14:42:40 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-05 17:13:29 +02:00
|
|
|
// id is used for lookup
|
|
|
|
// emoteName is used for giving a name to the emote in case it doesn't exist
|
2018-06-07 13:01:06 +02:00
|
|
|
util::EmoteData TwitchEmotes::getEmoteById(const QString &id, const QString &emoteName)
|
2018-06-05 17:13:29 +02:00
|
|
|
{
|
|
|
|
QString _emoteName = emoteName;
|
|
|
|
_emoteName.replace("<", "<");
|
|
|
|
_emoteName.replace(">", ">");
|
|
|
|
|
|
|
|
// clang-format off
|
|
|
|
static QMap<QString, QString> 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] {
|
|
|
|
util::EmoteData newEmoteData;
|
2018-06-24 18:29:30 +02:00
|
|
|
auto cleanCode = cleanUpCode(emoteName);
|
2018-06-05 17:13:29 +02:00
|
|
|
newEmoteData.image1x = new messages::Image(getEmoteLink(id, "1.0"), 1, emoteName,
|
|
|
|
_emoteName + "<br/>Twitch Emote 1x");
|
2018-06-24 18:29:30 +02:00
|
|
|
newEmoteData.image1x->setCopyString(cleanCode);
|
|
|
|
|
2018-06-05 17:13:29 +02:00
|
|
|
newEmoteData.image2x = new messages::Image(getEmoteLink(id, "2.0"), .5, emoteName,
|
|
|
|
_emoteName + "<br/>Twitch Emote 2x");
|
2018-06-24 18:29:30 +02:00
|
|
|
newEmoteData.image2x->setCopyString(cleanCode);
|
|
|
|
|
2018-06-05 17:13:29 +02:00
|
|
|
newEmoteData.image3x = new messages::Image(getEmoteLink(id, "3.0"), .25, emoteName,
|
|
|
|
_emoteName + "<br/>Twitch Emote 3x");
|
|
|
|
|
2018-06-24 18:29:30 +02:00
|
|
|
newEmoteData.image3x->setCopyString(cleanCode);
|
|
|
|
|
2018-06-05 17:13:29 +02:00
|
|
|
return newEmoteData;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
void TwitchEmotes::refresh(const std::shared_ptr<TwitchAccount> &user)
|
|
|
|
{
|
|
|
|
debug::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()) {
|
|
|
|
debug::Log("Missing Client ID or OAuth token");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-06-07 12:36:06 +02:00
|
|
|
TwitchAccountEmoteData &emoteData = this->emotes[roomID];
|
2018-06-05 17:13:29 +02:00
|
|
|
|
|
|
|
if (emoteData.filled) {
|
2018-06-07 13:01:06 +02:00
|
|
|
debug::Log("Emotes are already loaded for room id {}", roomID);
|
2018-06-05 17:13:29 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString url("https://api.twitch.tv/kraken/users/" + roomID + "/emotes");
|
|
|
|
|
2018-06-07 13:01:06 +02:00
|
|
|
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) {
|
2018-06-24 14:42:40 +02:00
|
|
|
auto emoteSet = std::make_shared<EmoteSet>();
|
|
|
|
|
|
|
|
emoteSet->key = it.key();
|
2018-06-07 13:01:06 +02:00
|
|
|
|
2018-06-24 14:42:40 +02:00
|
|
|
loadSetData(emoteSet);
|
2018-06-07 13:01:06 +02:00
|
|
|
|
|
|
|
for (QJsonValue emoteValue : it.value().toArray()) {
|
|
|
|
QJsonObject emoticon = emoteValue.toObject();
|
|
|
|
QString id = QString::number(emoticon["id"].toInt());
|
|
|
|
QString code = emoticon["code"].toString();
|
2018-06-24 14:42:40 +02:00
|
|
|
|
2018-06-19 22:04:12 +02:00
|
|
|
auto cleanCode = cleanUpCode(code);
|
2018-06-24 14:42:40 +02:00
|
|
|
emoteSet->emotes.emplace_back(id, cleanCode);
|
2018-06-19 22:04:12 +02:00
|
|
|
emoteData.emoteCodes.push_back(cleanCode);
|
2018-06-07 13:01:06 +02:00
|
|
|
|
|
|
|
util::EmoteData emote = this->getEmoteById(id, code);
|
|
|
|
emoteData.emotes.insert(code, emote);
|
2018-06-05 17:13:29 +02:00
|
|
|
}
|
|
|
|
|
2018-06-24 14:42:40 +02:00
|
|
|
emoteData.emoteSets.emplace_back(emoteSet);
|
2018-06-07 13:01:06 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
emoteData.filled = true;
|
|
|
|
};
|
|
|
|
|
|
|
|
util::twitch::getAuthorized(url, clientID, oauthToken, QThread::currentThread(), loadEmotes);
|
2018-06-05 17:13:29 +02:00
|
|
|
}
|
|
|
|
|
2018-06-24 14:42:40 +02:00
|
|
|
void TwitchEmotes::loadSetData(std::shared_ptr<TwitchEmotes::EmoteSet> emoteSet)
|
|
|
|
{
|
|
|
|
if (!emoteSet) {
|
|
|
|
debug::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;
|
|
|
|
}
|
|
|
|
|
|
|
|
debug::Log("Load twitch emote set data for {}..", emoteSet->key);
|
|
|
|
util::NetworkRequest req("https://braize.pajlada.com/chatterino/twitchemotes/set/" +
|
|
|
|
emoteSet->key + "/");
|
|
|
|
|
|
|
|
req.setRequestType(util::NetworkRequest::GetRequest);
|
|
|
|
|
|
|
|
req.onError([](int errorCode) -> bool {
|
|
|
|
debug::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;
|
|
|
|
}
|
|
|
|
|
|
|
|
debug::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();
|
|
|
|
}
|
|
|
|
|
2018-06-05 17:13:29 +02:00
|
|
|
} // namespace twitch
|
|
|
|
} // namespace providers
|
|
|
|
} // namespace chatterino
|