mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-21 22:24:07 +01:00
Initial custom and channel-specific cheermote parsing done
Needs more testing once the rendering pipeline is complete again Fixes #74
This commit is contained in:
parent
54502bc8b5
commit
584e6e5643
4 changed files with 390 additions and 48 deletions
|
@ -1,6 +1,4 @@
|
||||||
#include "resourcemanager.hpp"
|
#include "resourcemanager.hpp"
|
||||||
//#include "singletons/emotemanager.hpp"
|
|
||||||
//#include "singletons/windowmanager.hpp"
|
|
||||||
#include "util/urlfetch.hpp"
|
#include "util/urlfetch.hpp"
|
||||||
|
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
|
@ -15,6 +13,257 @@ inline messages::Image *lli(const char *pixmapPath, qreal scale = 1)
|
||||||
return new messages::Image(new QPixmap(pixmapPath), scale);
|
return new messages::Image(new QPixmap(pixmapPath), scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename Type>
|
||||||
|
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<Type>()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out = value.Get<Type>();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <>
|
||||||
|
inline bool ReadValue<QString>(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<std::vector<QString>>(const rapidjson::Value &object, const char *key,
|
||||||
|
std::vector<QString> &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<ResourceManager::JSONCheermoteSet> &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
|
} // namespace
|
||||||
|
|
||||||
ResourceManager::ResourceManager()
|
ResourceManager::ResourceManager()
|
||||||
|
@ -90,6 +339,48 @@ void ResourceManager::loadChannelData(const QString &roomID, bool bypassCache)
|
||||||
|
|
||||||
ch.loaded = true;
|
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()
|
void ResourceManager::loadDynamicTwitchBadges()
|
||||||
|
@ -153,5 +444,5 @@ void ResourceManager::loadChatterinoBadges()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace singletons
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "messages/image.hpp"
|
#include "util/emotemap.hpp"
|
||||||
|
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
@ -61,8 +63,47 @@ public:
|
||||||
messages::Image *buttonBan;
|
messages::Image *buttonBan;
|
||||||
messages::Image *buttonTimeout;
|
messages::Image *buttonTimeout;
|
||||||
|
|
||||||
|
struct JSONCheermoteSet {
|
||||||
|
QString prefix;
|
||||||
|
std::vector<QString> scales;
|
||||||
|
|
||||||
|
std::vector<QString> backgrounds;
|
||||||
|
std::vector<QString> states;
|
||||||
|
|
||||||
|
QString type;
|
||||||
|
QString updatedAt;
|
||||||
|
int priority;
|
||||||
|
|
||||||
|
struct CheermoteTier {
|
||||||
|
int minBits;
|
||||||
|
QString id;
|
||||||
|
QString color;
|
||||||
|
|
||||||
|
// Background State Scale
|
||||||
|
std::map<QString, std::map<QString, std::map<QString, messages::Image *>>> images;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<CheermoteTier> tiers;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Cheermote {
|
||||||
|
// a Cheermote indicates one tier
|
||||||
|
QColor color;
|
||||||
|
int minBits;
|
||||||
|
|
||||||
|
util::EmoteData emoteDataAnimated;
|
||||||
|
util::EmoteData emoteDataStatic;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CheermoteSet {
|
||||||
|
QRegularExpression regex;
|
||||||
|
std::vector<Cheermote> cheermotes;
|
||||||
|
};
|
||||||
|
|
||||||
struct Channel {
|
struct Channel {
|
||||||
std::map<std::string, BadgeSet> badgeSets;
|
std::map<std::string, BadgeSet> badgeSets;
|
||||||
|
std::vector<JSONCheermoteSet> jsonCheermoteSets;
|
||||||
|
std::vector<CheermoteSet> cheermoteSets;
|
||||||
|
|
||||||
bool loaded = false;
|
bool loaded = false;
|
||||||
};
|
};
|
||||||
|
@ -90,5 +131,5 @@ public:
|
||||||
void loadChatterinoBadges();
|
void loadChatterinoBadges();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace singletons
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
}
|
|
||||||
|
|
|
@ -80,9 +80,7 @@ MessagePtr TwitchMessageBuilder::parse()
|
||||||
this->parseHighlights();
|
this->parseHighlights();
|
||||||
}
|
}
|
||||||
|
|
||||||
// bits
|
QString bits;
|
||||||
QString bits = "";
|
|
||||||
|
|
||||||
auto iterator = this->tags.find("bits");
|
auto iterator = this->tags.find("bits");
|
||||||
if (iterator != this->tags.end()) {
|
if (iterator != this->tags.end()) {
|
||||||
bits = iterator.value().toString();
|
bits = iterator.value().toString();
|
||||||
|
@ -145,46 +143,8 @@ MessagePtr TwitchMessageBuilder::parse()
|
||||||
if (!emoteData.isValid()) { // is text
|
if (!emoteData.isValid()) { // is text
|
||||||
QString string = std::get<1>(tuple);
|
QString string = std::get<1>(tuple);
|
||||||
|
|
||||||
static QRegularExpression cheerRegex("cheer[1-9][0-9]*");
|
if (!bits.isEmpty() && this->tryParseCheermote(string)) {
|
||||||
|
// This string was parsed as a cheermote
|
||||||
// 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
|
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -690,6 +650,55 @@ void TwitchMessageBuilder::addChatterinoBadges()
|
||||||
->setTooltip(QString::fromStdString(badge->tooltip));
|
->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<EmoteElement>(cheermote.emoteDataAnimated, EmoteElement::BitsAnimated);
|
||||||
|
this->append<TextElement>(amount, EmoteElement::Text, cheermote.color);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// bool
|
// bool
|
||||||
// sortTwitchEmotes(const std::pair<long int, Image *> &a,
|
// sortTwitchEmotes(const std::pair<long int, Image *> &a,
|
||||||
// const std::pair<long int, Image *> &b)
|
// const std::pair<long int, Image *> &b)
|
||||||
|
|
|
@ -63,6 +63,7 @@ private:
|
||||||
|
|
||||||
void parseTwitchBadges();
|
void parseTwitchBadges();
|
||||||
void addChatterinoBadges();
|
void addChatterinoBadges();
|
||||||
|
bool tryParseCheermote(const QString &string);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace twitch
|
} // namespace twitch
|
||||||
|
|
Loading…
Reference in a new issue