mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Migrate Twitch badges to Helix (#4537)
Co-authored-by: iProdigy <8106344+iProdigy@users.noreply.github.com> Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
594477d8b6
commit
d6ef48d4ef
|
@ -12,6 +12,7 @@
|
|||
- Minor: Reply context now censors blocked users. (#4502)
|
||||
- Minor: Added system message for empty mod list. (#4546)
|
||||
- Minor: Added `/lowtrust` command to open the suspicious user activity feed in browser. (#4542)
|
||||
- Minor: Migrated badges to Helix API. (#4537)
|
||||
- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314)
|
||||
- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314)
|
||||
- Bugfix: Fixed an issue where context-menu items for zero-width emotes displayed the wrong provider. (#4460)
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include "common/QLogging.hpp"
|
||||
#include "messages/Emote.hpp"
|
||||
#include "messages/Image.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
#include "util/DisplayBadge.hpp"
|
||||
|
||||
#include <QBuffer>
|
||||
|
@ -29,25 +30,49 @@ void TwitchBadges::loadTwitchBadges()
|
|||
{
|
||||
assert(this->loaded_ == false);
|
||||
|
||||
QUrl url("https://badges.twitch.tv/v1/badges/global/display");
|
||||
getHelix()->getGlobalBadges(
|
||||
[this](auto globalBadges) {
|
||||
auto badgeSets = this->badgeSets_.access();
|
||||
|
||||
QUrlQuery urlQuery;
|
||||
urlQuery.addQueryItem("language", "en");
|
||||
url.setQuery(urlQuery);
|
||||
|
||||
NetworkRequest(url)
|
||||
.onSuccess([this](auto result) -> Outcome {
|
||||
auto root = result.parseJson();
|
||||
|
||||
this->parseTwitchBadges(root);
|
||||
for (const auto &badgeSet : globalBadges.badgeSets)
|
||||
{
|
||||
const auto &setID = badgeSet.setID;
|
||||
for (const auto &version : badgeSet.versions)
|
||||
{
|
||||
const auto &emote = Emote{
|
||||
EmoteName{},
|
||||
ImageSet{
|
||||
Image::fromUrl(version.imageURL1x, 1),
|
||||
Image::fromUrl(version.imageURL2x, .5),
|
||||
Image::fromUrl(version.imageURL4x, .25),
|
||||
},
|
||||
Tooltip{version.title},
|
||||
version.clickURL,
|
||||
};
|
||||
(*badgeSets)[setID][version.id] =
|
||||
std::make_shared<Emote>(emote);
|
||||
}
|
||||
}
|
||||
|
||||
this->loaded();
|
||||
return Success;
|
||||
})
|
||||
.onError([this](auto res) {
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Error loading Twitch Badges from the badges API:"
|
||||
<< res.status() << " - falling back to backup";
|
||||
},
|
||||
[this](auto error, auto message) {
|
||||
QString errorMessage("Failed to load global badges - ");
|
||||
|
||||
switch (error)
|
||||
{
|
||||
case HelixGetGlobalBadgesError::Forwarded: {
|
||||
errorMessage += message;
|
||||
}
|
||||
break;
|
||||
|
||||
// This would most likely happen if the service is down, or if the JSON payload returned has changed format
|
||||
case HelixGetGlobalBadgesError::Unknown: {
|
||||
errorMessage += "An unknown error has occurred.";
|
||||
}
|
||||
break;
|
||||
}
|
||||
qCWarning(chatterinoTwitch) << errorMessage;
|
||||
QFile file(":/twitch-badges.json");
|
||||
if (!file.open(QFile::ReadOnly))
|
||||
{
|
||||
|
@ -64,8 +89,7 @@ void TwitchBadges::loadTwitchBadges()
|
|||
this->parseTwitchBadges(doc.object());
|
||||
|
||||
this->loaded();
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
void TwitchBadges::parseTwitchBadges(QJsonObject root)
|
||||
|
@ -93,7 +117,8 @@ void TwitchBadges::parseTwitchBadges(QJsonObject root)
|
|||
{versionObj.value("image_url_4x").toString()}, .25),
|
||||
},
|
||||
Tooltip{versionObj.value("title").toString()},
|
||||
Url{versionObj.value("click_url").toString()}};
|
||||
Url{versionObj.value("click_url").toString()},
|
||||
};
|
||||
// "title"
|
||||
// "clickAction"
|
||||
|
||||
|
|
|
@ -50,9 +50,6 @@ private:
|
|||
|
||||
TwitchBadges();
|
||||
void loadTwitchBadges();
|
||||
/**
|
||||
* @brief Accepts a JSON blob from https://badges.twitch.tv/v1/badges/global/display and updates our badges with it
|
||||
**/
|
||||
void parseTwitchBadges(QJsonObject root);
|
||||
void loaded();
|
||||
void loadEmoteImage(const QString &name, ImagePtr image,
|
||||
|
|
|
@ -1282,50 +1282,71 @@ void TwitchChannel::cleanUpReplyThreads()
|
|||
|
||||
void TwitchChannel::refreshBadges()
|
||||
{
|
||||
auto url = Url{"https://badges.twitch.tv/v1/badges/channels/" +
|
||||
this->roomId() + "/display?language=en"};
|
||||
NetworkRequest(url.string)
|
||||
if (this->roomId().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
.onSuccess([this,
|
||||
weak = weakOf<Channel>(this)](auto result) -> Outcome {
|
||||
getHelix()->getChannelBadges(
|
||||
this->roomId(),
|
||||
// successCallback
|
||||
[this, weak = weakOf<Channel>(this)](auto channelBadges) {
|
||||
auto shared = weak.lock();
|
||||
if (!shared)
|
||||
return Failure;
|
||||
{
|
||||
// The channel has been closed inbetween us making the request and the request finishing
|
||||
return;
|
||||
}
|
||||
|
||||
auto badgeSets = this->badgeSets_.access();
|
||||
|
||||
auto jsonRoot = result.parseJson();
|
||||
|
||||
auto _ = jsonRoot["badge_sets"].toObject();
|
||||
for (auto jsonBadgeSet = _.begin(); jsonBadgeSet != _.end();
|
||||
jsonBadgeSet++)
|
||||
for (const auto &badgeSet : channelBadges.badgeSets)
|
||||
{
|
||||
auto &versions = (*badgeSets)[jsonBadgeSet.key()];
|
||||
|
||||
auto _set = jsonBadgeSet->toObject()["versions"].toObject();
|
||||
for (auto jsonVersion_ = _set.begin();
|
||||
jsonVersion_ != _set.end(); jsonVersion_++)
|
||||
const auto &setID = badgeSet.setID;
|
||||
for (const auto &version : badgeSet.versions)
|
||||
{
|
||||
auto jsonVersion = jsonVersion_->toObject();
|
||||
auto emote = std::make_shared<Emote>(Emote{
|
||||
auto emote = Emote{
|
||||
EmoteName{},
|
||||
ImageSet{
|
||||
Image::fromUrl(
|
||||
{jsonVersion["image_url_1x"].toString()}, 1),
|
||||
Image::fromUrl(
|
||||
{jsonVersion["image_url_2x"].toString()}, .5),
|
||||
Image::fromUrl(
|
||||
{jsonVersion["image_url_4x"].toString()}, .25)},
|
||||
Tooltip{jsonVersion["description"].toString()},
|
||||
Url{jsonVersion["clickURL"].toString()}});
|
||||
|
||||
versions.emplace(jsonVersion_.key(), emote);
|
||||
};
|
||||
Image::fromUrl(version.imageURL1x, 1),
|
||||
Image::fromUrl(version.imageURL2x, .5),
|
||||
Image::fromUrl(version.imageURL4x, .25),
|
||||
},
|
||||
Tooltip{version.title},
|
||||
version.clickURL,
|
||||
};
|
||||
(*badgeSets)[setID][version.id] =
|
||||
std::make_shared<Emote>(emote);
|
||||
}
|
||||
}
|
||||
},
|
||||
// failureCallback
|
||||
[this, weak = weakOf<Channel>(this)](auto error, auto message) {
|
||||
auto shared = weak.lock();
|
||||
if (!shared)
|
||||
{
|
||||
// The channel has been closed inbetween us making the request and the request finishing
|
||||
return;
|
||||
}
|
||||
|
||||
return Success;
|
||||
})
|
||||
.execute();
|
||||
QString errorMessage("Failed to load channel badges - ");
|
||||
|
||||
switch (error)
|
||||
{
|
||||
case HelixGetChannelBadgesError::Forwarded: {
|
||||
errorMessage += message;
|
||||
}
|
||||
break;
|
||||
|
||||
// This would most likely happen if the service is down, or if the JSON payload returned has changed format
|
||||
case HelixGetChannelBadgesError::Unknown: {
|
||||
errorMessage += "An unknown error has occurred.";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
this->addMessage(makeSystemMessage(errorMessage));
|
||||
});
|
||||
}
|
||||
|
||||
void TwitchChannel::refreshCheerEmotes()
|
||||
|
|
|
@ -2468,6 +2468,98 @@ void Helix::startCommercial(
|
|||
.execute();
|
||||
}
|
||||
|
||||
// Twitch global badges
|
||||
// https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges
|
||||
void Helix::getGlobalBadges(
|
||||
ResultCallback<HelixGlobalBadges> successCallback,
|
||||
FailureCallback<HelixGetGlobalBadgesError, QString> failureCallback)
|
||||
{
|
||||
using Error = HelixGetGlobalBadgesError;
|
||||
|
||||
this->makeRequest("chat/badges/global", QUrlQuery())
|
||||
.onSuccess([successCallback](auto result) -> Outcome {
|
||||
if (result.status() != 200)
|
||||
{
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Success result for getting global badges was "
|
||||
<< result.status() << "but we expected it to be 200";
|
||||
}
|
||||
|
||||
auto response = result.parseJson();
|
||||
successCallback(HelixGlobalBadges(response));
|
||||
return Success;
|
||||
})
|
||||
.onError([failureCallback](auto result) {
|
||||
auto obj = result.parseJson();
|
||||
auto message = obj.value("message").toString();
|
||||
|
||||
switch (result.status())
|
||||
{
|
||||
case 401: {
|
||||
failureCallback(Error::Forwarded, message);
|
||||
}
|
||||
break;
|
||||
|
||||
default: {
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Helix global badges, unhandled error data:"
|
||||
<< result.status() << result.getData() << obj;
|
||||
failureCallback(Error::Unknown, message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Badges for the `broadcasterID` channel
|
||||
// https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges
|
||||
void Helix::getChannelBadges(
|
||||
QString broadcasterID, ResultCallback<HelixChannelBadges> successCallback,
|
||||
FailureCallback<HelixGetChannelBadgesError, QString> failureCallback)
|
||||
{
|
||||
using Error = HelixGetChannelBadgesError;
|
||||
|
||||
QUrlQuery urlQuery;
|
||||
urlQuery.addQueryItem("broadcaster_id", broadcasterID);
|
||||
|
||||
this->makeRequest("chat/badges", urlQuery)
|
||||
.onSuccess([successCallback](auto result) -> Outcome {
|
||||
if (result.status() != 200)
|
||||
{
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Success result for getting badges was "
|
||||
<< result.status() << "but we expected it to be 200";
|
||||
}
|
||||
|
||||
auto response = result.parseJson();
|
||||
successCallback(HelixChannelBadges(response));
|
||||
return Success;
|
||||
})
|
||||
.onError([failureCallback](auto result) {
|
||||
auto obj = result.parseJson();
|
||||
auto message = obj.value("message").toString();
|
||||
|
||||
switch (result.status())
|
||||
{
|
||||
case 400:
|
||||
case 401: {
|
||||
failureCallback(Error::Forwarded, message);
|
||||
}
|
||||
break;
|
||||
|
||||
default: {
|
||||
qCWarning(chatterinoTwitch)
|
||||
<< "Helix channel badges, unhandled error data:"
|
||||
<< result.status() << result.getData() << obj;
|
||||
failureCallback(Error::Unknown, message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
NetworkRequest Helix::makeRequest(QString url, QUrlQuery urlQuery)
|
||||
{
|
||||
assert(!url.startsWith("/"));
|
||||
|
|
|
@ -384,6 +384,55 @@ struct HelixModerators {
|
|||
}
|
||||
};
|
||||
|
||||
struct HelixBadgeVersion {
|
||||
QString id;
|
||||
Url imageURL1x;
|
||||
Url imageURL2x;
|
||||
Url imageURL4x;
|
||||
QString title;
|
||||
Url clickURL;
|
||||
|
||||
explicit HelixBadgeVersion(const QJsonObject &jsonObject)
|
||||
: id(jsonObject.value("id").toString())
|
||||
, imageURL1x(Url{jsonObject.value("image_url_1x").toString()})
|
||||
, imageURL2x(Url{jsonObject.value("image_url_2x").toString()})
|
||||
, imageURL4x(Url{jsonObject.value("image_url_4x").toString()})
|
||||
, title(jsonObject.value("title").toString())
|
||||
, clickURL(Url{jsonObject.value("click_url").toString()})
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
struct HelixBadgeSet {
|
||||
QString setID;
|
||||
std::vector<HelixBadgeVersion> versions;
|
||||
|
||||
explicit HelixBadgeSet(const QJsonObject &json)
|
||||
: setID(json.value("set_id").toString())
|
||||
{
|
||||
const auto jsonVersions = json.value("versions").toArray();
|
||||
for (const auto &version : jsonVersions)
|
||||
{
|
||||
versions.emplace_back(version.toObject());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct HelixGlobalBadges {
|
||||
std::vector<HelixBadgeSet> badgeSets;
|
||||
|
||||
explicit HelixGlobalBadges(const QJsonObject &jsonObject)
|
||||
{
|
||||
const auto &data = jsonObject.value("data").toArray();
|
||||
for (const auto &set : data)
|
||||
{
|
||||
this->badgeSets.emplace_back(set.toObject());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
using HelixChannelBadges = HelixGlobalBadges;
|
||||
|
||||
enum class HelixAnnouncementColor {
|
||||
Blue,
|
||||
Green,
|
||||
|
@ -616,6 +665,15 @@ enum class HelixStartCommercialError {
|
|||
Forwarded,
|
||||
};
|
||||
|
||||
enum class HelixGetGlobalBadgesError {
|
||||
Unknown,
|
||||
|
||||
// The error message is forwarded directly from the Twitch API
|
||||
Forwarded,
|
||||
};
|
||||
|
||||
using HelixGetChannelBadgesError = HelixGetGlobalBadgesError;
|
||||
|
||||
class IHelix
|
||||
{
|
||||
public:
|
||||
|
@ -899,6 +957,21 @@ public:
|
|||
FailureCallback<HelixStartCommercialError, QString>
|
||||
failureCallback) = 0;
|
||||
|
||||
// Get global Twitch badges
|
||||
// https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges
|
||||
virtual void getGlobalBadges(
|
||||
ResultCallback<HelixGlobalBadges> successCallback,
|
||||
FailureCallback<HelixGetGlobalBadgesError, QString>
|
||||
failureCallback) = 0;
|
||||
|
||||
// Get badges for the `broadcasterID` channel
|
||||
// https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges
|
||||
virtual void getChannelBadges(
|
||||
QString broadcasterID,
|
||||
ResultCallback<HelixChannelBadges> successCallback,
|
||||
FailureCallback<HelixGetChannelBadgesError, QString>
|
||||
failureCallback) = 0;
|
||||
|
||||
virtual void update(QString clientId, QString oauthToken) = 0;
|
||||
|
||||
protected:
|
||||
|
@ -1184,6 +1257,19 @@ public:
|
|||
FailureCallback<HelixStartCommercialError, QString> failureCallback)
|
||||
final;
|
||||
|
||||
// Get global Twitch badges
|
||||
// https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges
|
||||
void getGlobalBadges(ResultCallback<HelixGlobalBadges> successCallback,
|
||||
FailureCallback<HelixGetGlobalBadgesError, QString>
|
||||
failureCallback) final;
|
||||
|
||||
// Get badges for the `broadcasterID` channel
|
||||
// https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges
|
||||
void getChannelBadges(QString broadcasterID,
|
||||
ResultCallback<HelixChannelBadges> successCallback,
|
||||
FailureCallback<HelixGetChannelBadgesError, QString>
|
||||
failureCallback) final;
|
||||
|
||||
void update(QString clientId, QString oauthToken) final;
|
||||
|
||||
static void initialize();
|
||||
|
|
|
@ -136,6 +136,22 @@ Used in:
|
|||
|
||||
- `providers/twitch/TwitchChannel.cpp` to resolve a chats available cheer emotes. This helps us parse incoming messages like `pajaCheer1000`
|
||||
|
||||
### Get Global Badges
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference/#get-global-chat-badges
|
||||
|
||||
Used in:
|
||||
|
||||
- `providers/twitch/TwitchBadges.cpp` to load global badges
|
||||
|
||||
### Get Channel Badges
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference/#get-channel-chat-badges
|
||||
|
||||
Used in:
|
||||
|
||||
- `providers/twitch/TwitchChannel.cpp` to load channel badges
|
||||
|
||||
### Get Emote Sets
|
||||
|
||||
URL: https://dev.twitch.tv/docs/api/reference#get-emote-sets
|
||||
|
|
|
@ -225,6 +225,21 @@ public:
|
|||
HelixFailureCallback failureCallback),
|
||||
(override));
|
||||
|
||||
// The extra parenthesis around the failure callback is because its type contains a comma
|
||||
MOCK_METHOD(
|
||||
void, getGlobalBadges,
|
||||
(ResultCallback<HelixGlobalBadges> successCallback,
|
||||
(FailureCallback<HelixGetGlobalBadgesError, QString> failureCallback)),
|
||||
(override));
|
||||
|
||||
// The extra parenthesis around the failure callback is because its type contains a comma
|
||||
MOCK_METHOD(void, getChannelBadges,
|
||||
(QString broadcasterID,
|
||||
ResultCallback<HelixChannelBadges> successCallback,
|
||||
(FailureCallback<HelixGetChannelBadgesError, QString>
|
||||
failureCallback)),
|
||||
(override));
|
||||
|
||||
// The extra parenthesis around the failure callback is because its type contains a comma
|
||||
MOCK_METHOD(void, updateUserChatColor,
|
||||
(QString userID, QString color,
|
||||
|
|
Loading…
Reference in a new issue