Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate Twitch badges to Helix #4537

Merged
merged 38 commits into from
Apr 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9abe788
Initial implementation of Helix.cpp & Helix.hpp
ZonianMidian Apr 11, 2023
ad94591
Add changelog item
ZonianMidian Apr 11, 2023
e538c4a
Class declaration for errors
ZonianMidian Apr 11, 2023
b997784
New implementations and fixes for Helix.cpp and Helix.hpp
ZonianMidian Apr 12, 2023
cdaf33c
Minor correction
ZonianMidian Apr 12, 2023
e0db1e4
Class correction for clickURL in Helix.hpp
ZonianMidian Apr 12, 2023
ae4e27a
Removed unnecessary Content-Type
ZonianMidian Apr 12, 2023
8c99efb
Merge branch 'helix-migration/twitch-badges' of https://github.com/Zo…
ZonianMidian Apr 12, 2023
cd0c55d
Virtual functions getGlobalBadges and getChannelBadges defined in IHelix
ZonianMidian Apr 12, 2023
ca8653d
Added missing
ZonianMidian Apr 12, 2023
8702f8a
Added mock method to MockHelix for HelixGlobalBadges and HelixChannel…
ZonianMidian Apr 13, 2023
88cb070
Added extra parenthesis in mock method
ZonianMidian Apr 13, 2023
a0841cb
Correction of file formatting
ZonianMidian Apr 14, 2023
1218dc3
Merge branch 'master' into helix-migration/twitch-badges
ZonianMidian Apr 14, 2023
4f8fb94
Merge branch 'master' into helix-migration/twitch-badges
ZonianMidian Apr 15, 2023
97c4f52
Implementation of getGlobalsBadges in TwitchBadges.cpp
ZonianMidian Apr 15, 2023
dbff8bb
Merge branch 'helix-migration/twitch-badges' of https://github.com/Zo…
ZonianMidian Apr 15, 2023
521f297
Merge branch 'master' into helix-migration/twitch-badges
ZonianMidian Apr 15, 2023
30811e9
Fixing error message in TwitchBadges.cpp
ZonianMidian Apr 16, 2023
91677d6
Merge branch 'helix-migration/twitch-badges' of https://github.com/Zo…
ZonianMidian Apr 16, 2023
eb59af8
Implementation of getChannelBadges in TwitchChannel.cpp
ZonianMidian Apr 16, 2023
58b07dd
README update on Helix to add the new endpoints
ZonianMidian Apr 16, 2023
20feb1c
nit: const ref HelixBadgeVersion constructor parameter
pajlada Apr 16, 2023
17ca5ea
nit: Rename `json_versions` to `jsonVersions`
pajlada Apr 16, 2023
17ac301
nit: rename `badge_version` to `badgeVersion`
pajlada Apr 16, 2023
f50dae6
nit: use emplace_back to in-place construct badges
pajlada Apr 16, 2023
1b4f04f
nit: Remove HelixGlobalBadges default constructor
pajlada Apr 16, 2023
85c3517
nit: rename badges member `data` to `badgeSets`
pajlada Apr 16, 2023
3a84760
nit: Change the rare helix error cases to be warnings
pajlada Apr 16, 2023
3672216
nit: add space inbetween using & the makeRequest/variable definitions
pajlada Apr 16, 2023
8ccfb99
nit: don't specify the network request type to get
pajlada Apr 16, 2023
35d90f3
fix: we must make sure the channel is alive when channel badges finish
pajlada Apr 16, 2023
ffeb297
nit: Reformat Emote creation
pajlada Apr 16, 2023
f1907c9
nit: Remove const ref in emote creation
pajlada Apr 16, 2023
f6c855b
nit: add back scale for 1x url
pajlada Apr 16, 2023
ef89d7c
nit: keep `EmoteName{}` for specifying an empty emote name
pajlada Apr 16, 2023
1f31077
nit: Don't recreate the global badges
pajlada Apr 16, 2023
cc50428
Merge remote-tracking branch 'origin/master' into helix-migration/twi…
pajlada Apr 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
63 changes: 44 additions & 19 deletions src/providers/twitch/TwitchBadges.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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");
Copy link
Contributor

@iProdigy iProdigy Apr 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a follow-up PR, we can convert twitch-badges.json to use the new helix structure (this would allow enable a refactor of parseTwitchBadges that leverages the new structs from this PR).

if (!file.open(QFile::ReadOnly))
{
Expand All @@ -64,8 +89,7 @@ void TwitchBadges::loadTwitchBadges()
this->parseTwitchBadges(doc.object());

this->loaded();
})
.execute();
});
}

void TwitchBadges::parseTwitchBadges(QJsonObject root)
Expand Down Expand Up @@ -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"

Expand Down
3 changes: 0 additions & 3 deletions src/providers/twitch/TwitchBadges.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ class TwitchBadges

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,
Expand Down
85 changes: 53 additions & 32 deletions src/providers/twitch/TwitchChannel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
92 changes: 92 additions & 0 deletions src/providers/twitch/api/Helix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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("/"));
Expand Down
Loading