Migrated cheermotes to Helix API (#2440)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
Paweł 2021-05-15 19:02:47 +02:00 committed by GitHub
parent 7c4c797dbc
commit 519855d852
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 401 deletions

View file

@ -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

View file

@ -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 \

View file

@ -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

View file

@ -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<Channel>(this)](auto result) -> Outcome {
getHelix()->getCheermotes(
this->roomId(),
[this, weak = weakOf<Channel>(this)](
const std::vector<HelixCheermoteSet> &cheermoteSets) -> Outcome {
auto shared = weak.lock();
if (!shared)
{
return Failure;
}
auto cheerEmoteSets = ParseCheermoteSets(result.parseRapidJson());
std::vector<CheerEmoteSet> 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>(
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>(
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()

View file

@ -1,321 +0,0 @@
#include "TwitchParseCheerEmotes.hpp"
#include <rapidjson/document.h>
#include <QString>
#include <vector>
namespace chatterino {
namespace {
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(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<JSONCheermoteSet> ParseCheermoteSets(const rapidjson::Document &d)
{
std::vector<JSONCheermoteSet> 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

View file

@ -1,37 +0,0 @@
#pragma once
#include <rapidjson/document.h>
#include <QString>
#include <map>
#include <vector>
#include "messages/Image.hpp"
namespace chatterino {
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, ImagePtr>>>
images;
};
std::vector<CheermoteTier> tiers;
};
std::vector<JSONCheermoteSet> ParseCheermoteSets(const rapidjson::Document &d);
} // namespace chatterino

View file

@ -722,6 +722,45 @@ void Helix::manageAutoModMessages(
.execute();
}
void Helix::getCheermotes(
QString broadcasterId,
ResultCallback<std::vector<HelixCheermoteSet>> 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<HelixCheermoteSet> 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("/"));

View file

@ -1,5 +1,6 @@
#pragma once
#include "common/Aliases.hpp"
#include "common/NetworkRequest.hpp"
#include <QJsonArray>
@ -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<HelixCheermoteTier> 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<void()> successCallback,
std::function<void(HelixAutoModMessageError)> failureCallback);
// https://dev.twitch.tv/docs/api/reference/#get-cheermotes
void getCheermotes(
QString broadcasterId,
ResultCallback<std::vector<HelixCheermoteSet>> successCallback,
HelixFailureCallback failureCallback);
void update(QString clientId, QString oauthToken);
static void initialize();

View file

@ -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
@ -157,6 +149,14 @@ Requires `moderator:manage:automod` scope
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.