Add cached emotes fallback for failed provider fetches (#6125)

This commit is contained in:
Elias A.
2025-04-12 13:56:04 +03:00
committed by GitHub
parent b59124702c
commit 7c8274ed56
12 changed files with 198 additions and 41 deletions
+1
View File
@@ -2,6 +2,7 @@
## Unversioned
- Minor: Added cached emotes fallback when fetching from a provider fails. (#6125)
- Minor: Add an option for the reduced opacity of message history. (#6121)
- Minor: Make paused chat indicator more visible, and fix its zoom behavior. (#6123)
- Minor: Added WebSocket API for plugins. (#6076)
+27 -4
View File
@@ -11,8 +11,11 @@
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Settings.hpp"
#include "util/Helpers.hpp"
#include <QJsonArray>
#include <QLoggingCategory>
#include <QStringLiteral>
#include <QThread>
namespace {
@@ -218,9 +221,19 @@ void BttvEmotes::loadEmotes()
return;
}
readProviderEmotesCache("global", "betterttv", [this](const auto &jsonDoc) {
auto emotes = this->global_.get();
auto pair = parseGlobalEmotes(jsonDoc.array(), *emotes);
if (pair.first)
{
this->setEmotes(std::make_shared<EmoteMap>(std::move(pair.second)));
}
});
NetworkRequest(QString(globalEmoteApiUrl))
.timeout(30000)
.onSuccess([this](auto result) {
writeProviderEmotesCache("global", "betterttv", result.getData());
auto emotes = this->global_.get();
auto pair = parseGlobalEmotes(result.parseJsonArray(), *emotes);
if (pair.first)
@@ -229,6 +242,10 @@ void BttvEmotes::loadEmotes()
std::make_shared<EmoteMap>(std::move(pair.second)));
}
})
.onError([](auto result) {
qCWarning(chatterinoBttv) << "Failed to fetch global BTTV emotes. "
<< result.formatError();
})
.execute();
}
@@ -241,15 +258,16 @@ void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
const QString &channelId,
const QString &channelDisplayName,
std::function<void(EmoteMap &&)> callback,
bool manualRefresh)
bool manualRefresh, bool cacheHit)
{
NetworkRequest(QString(bttvChannelEmoteApiUrl) + channelId)
.timeout(20000)
.onSuccess([callback = std::move(callback), channel, channelDisplayName,
manualRefresh](auto result) {
.onSuccess([callback = std::move(callback), channel, channelId,
channelDisplayName, manualRefresh](auto result) {
auto emotes =
parseChannelEmotes(result.parseJson(), channelDisplayName);
bool hasEmotes = !emotes.empty();
writeProviderEmotesCache(channelId, "betterttv", result.getData());
callback(std::move(emotes));
if (auto shared = channel.lock(); manualRefresh)
@@ -265,7 +283,7 @@ void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
}
}
})
.onError([channelId, channel, manualRefresh](auto result) {
.onError([channelId, channel, manualRefresh, cacheHit](auto result) {
auto shared = channel.lock();
if (!shared)
{
@@ -291,6 +309,11 @@ void BttvEmotes::loadChannel(std::weak_ptr<Channel> channel,
QStringLiteral("Failed to fetch BetterTTV channel "
"emotes. (Error: %1)")
.arg(errorString));
if (cacheHit)
{
shared->addSystemMessage(
"Using cached BetterTTV emotes as fallback.");
}
}
})
.execute();
+1 -1
View File
@@ -47,7 +47,7 @@ public:
const QString &channelId,
const QString &channelDisplayName,
std::function<void(EmoteMap &&)> callback,
bool manualRefresh);
bool manualRefresh, bool cacheHit);
/**
* Adds an emote to the `channelEmoteMap`.
+48 -28
View File
@@ -9,6 +9,7 @@
#include "providers/ffz/FfzUtil.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Settings.hpp"
#include "util/Helpers.hpp"
namespace {
@@ -237,15 +238,26 @@ void FfzEmotes::loadEmotes()
return;
}
readProviderEmotesCache("global", "frankerfacez", [this](auto jsonDoc) {
auto parsedSet = parseGlobalEmotes(jsonDoc.object());
this->setEmotes(std::make_shared<EmoteMap>(std::move(parsedSet)));
});
QString url("https://api.frankerfacez.com/v1/set/global");
NetworkRequest(url)
.timeout(30000)
.onSuccess([this](auto result) {
writeProviderEmotesCache("global", "frankerfacez",
result.getData());
auto parsedSet = parseGlobalEmotes(result.parseJson());
this->setEmotes(std::make_shared<EmoteMap>(std::move(parsedSet)));
})
.onError([](auto result) {
qCWarning(chatterinoFfzemotes)
<< "Failed to fetch global FFZ emotes. "
<< result.formatError();
})
.execute();
}
@@ -260,7 +272,7 @@ void FfzEmotes::loadChannel(
std::function<void(std::optional<EmotePtr>)> modBadgeCallback,
std::function<void(std::optional<EmotePtr>)> vipBadgeCallback,
std::function<void(FfzChannelBadgeMap &&)> channelBadgesCallback,
bool manualRefresh)
bool manualRefresh, bool cacheHit)
{
qCDebug(LOG) << "Reload FFZ Channel Emotes for channel" << channelID;
@@ -271,7 +283,9 @@ void FfzEmotes::loadChannel(
modBadgeCallback = std::move(modBadgeCallback),
vipBadgeCallback = std::move(vipBadgeCallback),
channelBadgesCallback = std::move(channelBadgesCallback),
channel, manualRefresh](const auto &result) {
channel, channelID, manualRefresh](const auto &result) {
writeProviderEmotesCache(channelID, "frankerfacez",
result.getData());
const auto json = result.parseJson();
auto emoteMap = parseChannelEmotes(json);
@@ -301,33 +315,39 @@ void FfzEmotes::loadChannel(
}
}
})
.onError([channelID, channel, manualRefresh](const auto &result) {
auto shared = channel.lock();
if (!shared)
{
return;
}
if (result.status() == 404)
{
// User does not have any FFZ emotes
if (manualRefresh)
.onError(
[channelID, channel, manualRefresh, cacheHit](const auto &result) {
auto shared = channel.lock();
if (!shared)
{
shared->addSystemMessage(CHANNEL_HAS_NO_EMOTES);
return;
}
}
else
{
// TODO: Auto retry in case of a timeout, with a delay
auto errorString = result.formatError();
qCWarning(LOG) << "Error fetching FFZ emotes for channel"
<< channelID << ", error" << errorString;
shared->addSystemMessage(
QStringLiteral("Failed to fetch FrankerFaceZ channel "
"emotes. (Error: %1)")
.arg(errorString));
}
})
if (result.status() == 404)
{
// User does not have any FFZ emotes
if (manualRefresh)
{
shared->addSystemMessage(CHANNEL_HAS_NO_EMOTES);
}
}
else
{
// TODO: Auto retry in case of a timeout, with a delay
auto errorString = result.formatError();
qCWarning(LOG) << "Error fetching FFZ emotes for channel"
<< channelID << ", error" << errorString;
shared->addSystemMessage(
QStringLiteral("Failed to fetch FrankerFaceZ channel "
"emotes. (Error: %1)")
.arg(errorString));
if (cacheHit)
{
shared->addSystemMessage(
"Using cached FrankerFaceZ emotes as fallback.");
}
}
})
.execute();
}
+1 -1
View File
@@ -51,7 +51,7 @@ public:
std::function<void(std::optional<EmotePtr>)> modBadgeCallback,
std::function<void(std::optional<EmotePtr>)> vipBadgeCallback,
std::function<void(FfzChannelBadgeMap &&)> channelBadgesCallback,
bool manualRefresh);
bool manualRefresh, bool cacheHit);
private:
Atomic<std::shared_ptr<const EmoteMap>> global_;
+19 -2
View File
@@ -16,6 +16,8 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStringView>
#include <QThread>
#include <array>
@@ -230,11 +232,18 @@ void SeventvEmotes::loadGlobalEmotes()
return;
}
readProviderEmotesCache("global", "seventv", [this](auto jsonDoc) {
auto emoteMap = parseEmotes(jsonDoc.object()["emotes"].toArray(), true);
this->setGlobalEmotes(std::make_shared<EmoteMap>(std::move(emoteMap)));
});
qCDebug(chatterinoSeventv) << "Loading 7TV Global Emotes";
getApp()->getSeventvAPI()->getEmoteSet(
u"global"_s,
[this](const auto &json) {
writeProviderEmotesCache("global", "seventv",
QJsonDocument(json).toJson());
QJsonArray parsedEmotes = json["emotes"].toArray();
auto emoteMap = parseEmotes(parsedEmotes, true);
@@ -256,7 +265,8 @@ void SeventvEmotes::setGlobalEmotes(std::shared_ptr<const EmoteMap> emotes)
void SeventvEmotes::loadChannelEmotes(
const std::weak_ptr<Channel> &channel, const QString &channelId,
std::function<void(EmoteMap &&, ChannelInfo)> callback, bool manualRefresh)
std::function<void(EmoteMap &&, ChannelInfo)> callback, bool manualRefresh,
bool cacheHit)
{
qCDebug(chatterinoSeventv)
<< "Reloading 7TV Channel Emotes" << channelId << manualRefresh;
@@ -265,6 +275,8 @@ void SeventvEmotes::loadChannelEmotes(
channelId,
[callback = std::move(callback), channel, channelId,
manualRefresh](const auto &json) {
writeProviderEmotesCache(channelId, "seventv",
QJsonDocument(json).toJson());
const auto emoteSet = json["emote_set"].toObject();
const auto parsedEmotes = emoteSet["emotes"].toArray();
@@ -312,7 +324,7 @@ void SeventvEmotes::loadChannelEmotes(
}
}
},
[channelId, channel, manualRefresh](const auto &result) {
[channelId, channel, manualRefresh, cacheHit](const auto &result) {
auto shared = channel.lock();
if (!shared)
{
@@ -339,6 +351,11 @@ void SeventvEmotes::loadChannelEmotes(
QStringLiteral("Failed to fetch 7TV channel "
"emotes. (Error: %1)")
.arg(errorString));
if (cacheHit)
{
shared->addSystemMessage(
"Using cached 7TV emotes as fallback.");
}
}
});
}
+1 -1
View File
@@ -108,7 +108,7 @@ public:
static void loadChannelEmotes(
const std::weak_ptr<Channel> &channel, const QString &channelId,
std::function<void(EmoteMap &&, ChannelInfo)> callback,
bool manualRefresh);
bool manualRefresh, bool cacheHit);
/**
* Adds an emote to the `map` if it's valid.
+29 -3
View File
@@ -327,6 +327,17 @@ void TwitchChannel::refreshBTTVChannelEmotes(bool manualRefresh)
return;
}
bool cacheHit = readProviderEmotesCache(
this->roomId(), "betterttv",
[this, weak = weakOf<Channel>(this)](auto jsonDoc) {
if (auto shared = weak.lock())
{
auto emoteMap = bttv::detail::parseChannelEmotes(
jsonDoc.object(), this->getLocalizedName());
this->setBttvEmotes(std::make_shared<const EmoteMap>(emoteMap));
}
});
BttvEmotes::loadChannel(
weakOf<Channel>(this), this->roomId(), this->getLocalizedName(),
[this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
@@ -335,7 +346,7 @@ void TwitchChannel::refreshBTTVChannelEmotes(bool manualRefresh)
this->setBttvEmotes(std::make_shared<const EmoteMap>(emoteMap));
}
},
manualRefresh);
manualRefresh, cacheHit);
}
void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh)
@@ -346,6 +357,12 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh)
return;
}
bool cacheHit = readProviderEmotesCache(
this->roomId(), "frankerfacez", [this](const auto &jsonDoc) {
auto emoteMap = ffz::detail::parseChannelEmotes(jsonDoc.object());
this->setFfzEmotes(std::make_shared<const EmoteMap>(emoteMap));
});
FfzEmotes::loadChannel(
weakOf<Channel>(this), this->roomId(),
[this, weak = weakOf<Channel>(this)](auto &&emoteMap) {
@@ -376,7 +393,7 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh)
std::forward<decltype(channelBadges)>(channelBadges);
}
},
manualRefresh);
manualRefresh, cacheHit);
}
void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh)
@@ -387,6 +404,15 @@ void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh)
return;
}
bool cacheHit = readProviderEmotesCache(
this->roomId(), "seventv", [this](auto jsonDoc) {
const auto json = jsonDoc.object();
const auto emoteSet = json["emote_set"].toObject();
const auto parsedEmotes = emoteSet["emotes"].toArray();
auto emoteMap = seventv::detail::parseEmotes(parsedEmotes, false);
this->setSeventvEmotes(std::make_shared<const EmoteMap>(emoteMap));
});
SeventvEmotes::loadChannelEmotes(
weakOf<Channel>(this), this->roomId(),
[this, weak = weakOf<Channel>(this)](auto &&emoteMap,
@@ -401,7 +427,7 @@ void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh)
channelInfo.twitchConnectionIndex;
}
},
manualRefresh);
manualRefresh, cacheHit);
}
void TwitchChannel::setBttvEmotes(std::shared_ptr<const EmoteMap> &&map)
+6 -1
View File
@@ -39,7 +39,7 @@ QString Paths::cacheDirectory() const
static const auto pathSetting = [] {
QStringSetting cachePathSetting("/cache/path");
cachePathSetting.connect([](const auto &newPath, auto) {
cachePathSetting.connect([](const auto &newPath) {
if (!newPath.isEmpty())
{
QDir().mkpath(newPath);
@@ -59,6 +59,11 @@ QString Paths::cacheDirectory() const
return path;
}
QString Paths::cacheFilePath(const QString &fileName) const
{
return combinePath(this->cacheDirectory(), fileName);
}
void Paths::initAppFilePathHash()
{
this->applicationFilePathHash =
+5
View File
@@ -50,6 +50,11 @@ public:
QString cacheDirectory() const;
/// Returns the full file path for a file in the cache directory
///
/// e.g. cacheFilePath("foo") will return <cacheDirectory>/foo
QString cacheFilePath(const QString &fileName) const;
private:
void initAppFilePathHash();
void initCheckPortable();
+52
View File
@@ -1,12 +1,19 @@
#include "util/Helpers.hpp"
#include "Application.hpp"
#include "common/QLogging.hpp"
#include "providers/twitch/TwitchCommon.hpp"
#include "singletons/Paths.hpp"
#include <QDateTime>
#include <QDirIterator>
#include <QJsonObject>
#include <QLocale>
#include <QLoggingCategory>
#include <QRegularExpression>
#include <QStringBuilder>
#include <QStringView>
#include <QThreadPool>
#include <QTimeZone>
#include <QUuid>
@@ -386,4 +393,49 @@ void removeLastQS(QString &str)
#endif
}
void writeProviderEmotesCache(const QString &id, const QString &provider,
const QByteArray &bytes)
{
QThreadPool::globalInstance()->start([bytes, id, provider]() {
auto cacheKey = id % "." % provider;
QFile responseCache(getApp()->getPaths().cacheFilePath(cacheKey));
if (responseCache.open(QIODevice::WriteOnly))
{
qCDebug(chatterinoCache)
<< "Saved json response " << id << "." << provider;
responseCache.write(qCompress(bytes));
}
});
}
bool readProviderEmotesCache(const QString &id, const QString &provider,
const std::function<void(QJsonDocument)> &callback)
{
auto cacheKey = id % "." % provider;
QFile responseCache(getApp()->getPaths().cacheFilePath(cacheKey));
if (responseCache.open(QIODevice::ReadOnly))
{
QJsonParseError parseError;
auto doc = QJsonDocument::fromJson(qUncompress(responseCache.readAll()),
&parseError);
if (parseError.error != QJsonParseError::NoError)
{
qCWarning(chatterinoCache)
<< "Emote cache " << id << "." << provider
<< " parsing failed: " << parseError.errorString();
}
qCDebug(chatterinoCache)
<< "Loaded emote cache: " << id << "." << provider;
callback(doc);
return true;
}
// If the API call fails, we need to know if loading cached emotes was successful
return false;
}
} // namespace chatterino
+8
View File
@@ -1,6 +1,7 @@
#pragma once
#include <QColor>
#include <QJsonDocument>
#include <QLocale>
#include <QString>
@@ -218,4 +219,11 @@ void removeFirstQS(QString &str);
/// @param str The Qt string we want to remove 1 character from
void removeLastQS(QString &str);
void writeProviderEmotesCache(const QString &id, const QString &provider,
const QByteArray &bytes);
bool readProviderEmotesCache(
const QString &id, const QString &provider,
const std::function<void(QJsonDocument)> &callback);
} // namespace chatterino