Use New 7TV Cosmetics System (#4512)

* feat(seventv): use new cosmetics system

* chore: add changelog entry

* fix: old `clang-format`

* fix: small suggestions pt1

* refactor: add 7tv api wrapper

* fix: small clang-tidy things

* fix: remove unused constants

* fix: old clangtidy

* refactor: rename

* fix: increase interval to 60s

* fix: newline

* fix: Twitch

* docs: add comment

* fix: remove v2 badges endpoint

* fix: deadlock

This is actually really sad.

* fix: remove api entry

* fix: old clang-format

* Sort functions in SeventvBadges.hpp/cpp

* Remove unused vector include

* Add comments to SeventvBadges.hpp functions

* Rename `addBadge` to `registerBadge`

* fix: cleanup eventloop

* ci(test): add timeout

---------

Co-authored-by: Felanbird <41973452+Felanbird@users.noreply.github.com>
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix
2023-07-29 11:49:44 +02:00
committed by GitHub
parent 8cfa5e866e
commit 33fa3e0a97
25 changed files with 828 additions and 189 deletions
+117 -132
View File
@@ -1,6 +1,6 @@
#include "providers/seventv/SeventvEmotes.hpp"
#include "common/NetworkRequest.hpp"
#include "common/Literals.hpp"
#include "common/NetworkResult.hpp"
#include "common/QLogging.hpp"
#include "messages/Emote.hpp"
@@ -8,6 +8,7 @@
#include "messages/ImageSet.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/seventv/eventapi/Dispatch.hpp"
#include "providers/seventv/SeventvAPI.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Settings.hpp"
@@ -36,10 +37,6 @@ using namespace seventv::eventapi;
const QString CHANNEL_HAS_NO_EMOTES("This channel has no 7TV channel emotes.");
const QString EMOTE_LINK_FORMAT("https://7tv.app/emotes/%1");
const QString API_URL_USER("https://7tv.io/v3/users/twitch/%1");
const QString API_URL_GLOBAL_EMOTE_SET("https://7tv.io/v3/emote-sets/global");
const QString API_URL_EMOTE_SET("https://7tv.io/v3/emote-sets/%1");
struct CreateEmoteResult {
Emote emote;
EmoteId id;
@@ -77,71 +74,6 @@ bool isZeroWidthRecommended(const QJsonObject &emoteData)
return flags.has(SeventvEmoteFlag::ZeroWidth);
}
ImageSet makeImageSet(const QJsonObject &emoteData)
{
auto host = emoteData["host"].toObject();
// "//cdn.7tv[...]"
auto baseUrl = host["url"].toString();
auto files = host["files"].toArray();
// TODO: emit four images
std::array<ImagePtr, 3> sizes;
double baseWidth = 0.0;
int nextSize = 0;
for (auto fileItem : files)
{
if (nextSize >= sizes.size())
{
break;
}
auto file = fileItem.toObject();
if (file["format"].toString() != "WEBP")
{
continue; // We only use webp
}
double width = file["width"].toDouble();
double scale = 1.0; // in relation to first image
if (baseWidth > 0.0)
{
scale = baseWidth / width;
}
else
{
// => this is the first image
baseWidth = width;
}
auto image = Image::fromUrl(
{QString("https:%1/%2").arg(baseUrl, file["name"].toString())},
scale);
sizes.at(nextSize) = image;
nextSize++;
}
if (nextSize < sizes.size())
{
// this should be really rare
// this means we didn't get all sizes of an emote
if (nextSize == 0)
{
qCDebug(chatterinoSeventv)
<< "Got file list without any eligible files";
// When this emote is typed, chatterino will crash.
return ImageSet{};
}
for (; nextSize < sizes.size(); nextSize++)
{
sizes.at(nextSize) = Image::getEmpty();
}
}
return ImageSet{sizes[0], sizes[1], sizes[2]};
}
Tooltip createTooltip(const QString &name, const QString &author, bool isGlobal)
{
return Tooltip{QString("%1<br>%2 7TV Emote<br>By: %3")
@@ -172,7 +104,7 @@ CreateEmoteResult createEmote(const QJsonObject &activeEmote,
? createAliasedTooltip(emoteName.string, baseEmoteName.string,
author.string, isGlobal)
: createTooltip(emoteName.string, author.string, isGlobal);
auto imageSet = makeImageSet(emoteData);
auto imageSet = SeventvEmotes::createImageSet(emoteData);
auto emote =
Emote({emoteName, imageSet, tooltip,
@@ -247,6 +179,7 @@ EmotePtr createUpdatedEmote(const EmotePtr &oldEmote,
namespace chatterino {
using namespace seventv::eventapi;
using namespace literals;
SeventvEmotes::SeventvEmotes()
: global_(std::make_shared<EmoteMap>())
@@ -281,24 +214,21 @@ void SeventvEmotes::loadGlobalEmotes()
qCDebug(chatterinoSeventv) << "Loading 7TV Global Emotes";
NetworkRequest(API_URL_GLOBAL_EMOTE_SET, NetworkRequestType::Get)
.timeout(30000)
.onSuccess([this](const NetworkResult &result) -> Outcome {
QJsonArray parsedEmotes = result.parseJson()["emotes"].toArray();
getSeventvAPI().getEmoteSet(
u"global"_s,
[this](const auto &json) {
QJsonArray parsedEmotes = json["emotes"].toArray();
auto emoteMap = parseEmotes(parsedEmotes, true);
qCDebug(chatterinoSeventv)
<< "Loaded" << emoteMap.size() << "7TV Global Emotes";
this->setGlobalEmotes(
std::make_shared<EmoteMap>(std::move(emoteMap)));
return Success;
})
.onError([](const NetworkResult &result) {
},
[](const auto &result) {
qCWarning(chatterinoSeventv)
<< "Couldn't load 7TV global emotes" << result.getData();
})
.execute();
});
}
void SeventvEmotes::setGlobalEmotes(std::shared_ptr<const EmoteMap> emotes)
@@ -313,13 +243,12 @@ void SeventvEmotes::loadChannelEmotes(
qCDebug(chatterinoSeventv)
<< "Reloading 7TV Channel Emotes" << channelId << manualRefresh;
NetworkRequest(API_URL_USER.arg(channelId), NetworkRequestType::Get)
.timeout(20000)
.onSuccess([callback = std::move(callback), channel, channelId,
manualRefresh](const NetworkResult &result) -> Outcome {
auto json = result.parseJson();
auto emoteSet = json["emote_set"].toObject();
auto parsedEmotes = emoteSet["emotes"].toArray();
getSeventvAPI().getUserByTwitchID(
channelId,
[callback = std::move(callback), channel, channelId,
manualRefresh](const auto &json) {
const auto emoteSet = json["emote_set"].toObject();
const auto parsedEmotes = emoteSet["emotes"].toArray();
auto emoteMap = parseEmotes(parsedEmotes, false);
bool hasEmotes = !emoteMap.empty();
@@ -350,7 +279,7 @@ void SeventvEmotes::loadChannelEmotes(
auto shared = channel.lock();
if (!shared)
{
return Success;
return;
}
if (manualRefresh)
@@ -366,40 +295,37 @@ void SeventvEmotes::loadChannelEmotes(
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
}
}
return Success;
})
.onError(
[channelId, channel, manualRefresh](const NetworkResult &result) {
auto shared = channel.lock();
if (!shared)
},
[channelId, channel, manualRefresh](const auto &result) {
auto shared = channel.lock();
if (!shared)
{
return;
}
if (result.status() == 404)
{
qCWarning(chatterinoSeventv)
<< "Error occurred fetching 7TV emotes: "
<< result.parseJson();
if (manualRefresh)
{
return;
shared->addMessage(
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
}
if (result.status() == 404)
{
qCWarning(chatterinoSeventv)
<< "Error occurred fetching 7TV emotes: "
<< result.parseJson();
if (manualRefresh)
{
shared->addMessage(
makeSystemMessage(CHANNEL_HAS_NO_EMOTES));
}
}
else
{
// TODO: Auto retry in case of a timeout, with a delay
auto errorString = result.formatError();
qCWarning(chatterinoSeventv)
<< "Error fetching 7TV emotes for channel" << channelId
<< ", error" << errorString;
shared->addMessage(makeSystemMessage(
QStringLiteral("Failed to fetch 7TV channel "
"emotes. (Error: %1)")
.arg(errorString)));
}
})
.execute();
}
else
{
// TODO: Auto retry in case of a timeout, with a delay
auto errorString = result.formatError();
qCWarning(chatterinoSeventv)
<< "Error fetching 7TV emotes for channel" << channelId
<< ", error" << errorString;
shared->addMessage(makeSystemMessage(
QStringLiteral("Failed to fetch 7TV channel "
"emotes. (Error: %1)")
.arg(errorString)));
}
});
}
boost::optional<EmotePtr> SeventvEmotes::addEmote(
@@ -479,11 +405,9 @@ void SeventvEmotes::getEmoteSet(
{
qCDebug(chatterinoSeventv) << "Loading 7TV Emote Set" << emoteSetId;
NetworkRequest(API_URL_EMOTE_SET.arg(emoteSetId), NetworkRequestType::Get)
.timeout(20000)
.onSuccess([callback = std::move(successCallback),
emoteSetId](const NetworkResult &result) -> Outcome {
auto json = result.parseJson();
getSeventvAPI().getEmoteSet(
emoteSetId,
[callback = std::move(successCallback), emoteSetId](const auto &json) {
auto parsedEmotes = json["emotes"].toArray();
auto emoteMap = parseEmotes(parsedEmotes, false);
@@ -492,13 +416,74 @@ void SeventvEmotes::getEmoteSet(
<< "7TV Emotes from" << emoteSetId;
callback(std::move(emoteMap), json["name"].toString());
return Success;
})
.onError([emoteSetId, callback = std::move(errorCallback)](
const NetworkResult &result) {
},
[emoteSetId, callback = std::move(errorCallback)](const auto &result) {
callback(result.formatError());
})
.execute();
});
}
ImageSet SeventvEmotes::createImageSet(const QJsonObject &emoteData)
{
auto host = emoteData["host"].toObject();
// "//cdn.7tv[...]"
auto baseUrl = host["url"].toString();
auto files = host["files"].toArray();
std::array<ImagePtr, 3> sizes;
double baseWidth = 0.0;
size_t nextSize = 0;
for (auto fileItem : files)
{
if (nextSize >= sizes.size())
{
break;
}
auto file = fileItem.toObject();
if (file["format"].toString() != "WEBP")
{
continue; // We only use webp
}
double width = file["width"].toDouble();
double scale = 1.0; // in relation to first image
if (baseWidth > 0.0)
{
scale = baseWidth / width;
}
else
{
// => this is the first image
baseWidth = width;
}
auto image = Image::fromUrl(
{QString("https:%1/%2").arg(baseUrl, file["name"].toString())},
scale);
sizes.at(nextSize) = image;
nextSize++;
}
if (nextSize < sizes.size())
{
// this should be really rare
// this means we didn't get all sizes of an emote
if (nextSize == 0)
{
qCDebug(chatterinoSeventv)
<< "Got file list without any eligible files";
// When this emote is typed, chatterino will crash.
return ImageSet{};
}
for (; nextSize < sizes.size(); nextSize++)
{
sizes.at(nextSize) = Image::getEmpty();
}
}
return ImageSet{sizes[0], sizes[1], sizes[2]};
}
} // namespace chatterino