From 7c8274ed569f1c0c07cb46c0d30b7c3f3a52474e Mon Sep 17 00:00:00 2001 From: "Elias A." <69414142+occluder@users.noreply.github.com> Date: Sat, 12 Apr 2025 13:56:04 +0300 Subject: [PATCH] Add cached emotes fallback for failed provider fetches (#6125) --- CHANGELOG.md | 1 + src/providers/bttv/BttvEmotes.cpp | 31 ++++++++-- src/providers/bttv/BttvEmotes.hpp | 2 +- src/providers/ffz/FfzEmotes.cpp | 76 ++++++++++++++++--------- src/providers/ffz/FfzEmotes.hpp | 2 +- src/providers/seventv/SeventvEmotes.cpp | 21 ++++++- src/providers/seventv/SeventvEmotes.hpp | 2 +- src/providers/twitch/TwitchChannel.cpp | 32 ++++++++++- src/singletons/Paths.cpp | 7 ++- src/singletons/Paths.hpp | 5 ++ src/util/Helpers.cpp | 52 +++++++++++++++++ src/util/Helpers.hpp | 8 +++ 12 files changed, 198 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 554a4d24..99ea2344 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/src/providers/bttv/BttvEmotes.cpp b/src/providers/bttv/BttvEmotes.cpp index 8e966364..40c2b2e7 100644 --- a/src/providers/bttv/BttvEmotes.cpp +++ b/src/providers/bttv/BttvEmotes.cpp @@ -11,8 +11,11 @@ #include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "singletons/Settings.hpp" +#include "util/Helpers.hpp" #include +#include +#include #include 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(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(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, const QString &channelId, const QString &channelDisplayName, std::function 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, } } }) - .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, QStringLiteral("Failed to fetch BetterTTV channel " "emotes. (Error: %1)") .arg(errorString)); + if (cacheHit) + { + shared->addSystemMessage( + "Using cached BetterTTV emotes as fallback."); + } } }) .execute(); diff --git a/src/providers/bttv/BttvEmotes.hpp b/src/providers/bttv/BttvEmotes.hpp index 788b1e43..92e4cf37 100644 --- a/src/providers/bttv/BttvEmotes.hpp +++ b/src/providers/bttv/BttvEmotes.hpp @@ -47,7 +47,7 @@ public: const QString &channelId, const QString &channelDisplayName, std::function callback, - bool manualRefresh); + bool manualRefresh, bool cacheHit); /** * Adds an emote to the `channelEmoteMap`. diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index 84887186..10d4b165 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -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(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(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)> modBadgeCallback, std::function)> vipBadgeCallback, std::function 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(); } diff --git a/src/providers/ffz/FfzEmotes.hpp b/src/providers/ffz/FfzEmotes.hpp index 4c42ecbe..f63efe36 100644 --- a/src/providers/ffz/FfzEmotes.hpp +++ b/src/providers/ffz/FfzEmotes.hpp @@ -51,7 +51,7 @@ public: std::function)> modBadgeCallback, std::function)> vipBadgeCallback, std::function channelBadgesCallback, - bool manualRefresh); + bool manualRefresh, bool cacheHit); private: Atomic> global_; diff --git a/src/providers/seventv/SeventvEmotes.cpp b/src/providers/seventv/SeventvEmotes.cpp index 6cc53b3c..1ed27984 100644 --- a/src/providers/seventv/SeventvEmotes.cpp +++ b/src/providers/seventv/SeventvEmotes.cpp @@ -16,6 +16,8 @@ #include #include +#include +#include #include #include @@ -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(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 emotes) void SeventvEmotes::loadChannelEmotes( const std::weak_ptr &channel, const QString &channelId, - std::function callback, bool manualRefresh) + std::function 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."); + } } }); } diff --git a/src/providers/seventv/SeventvEmotes.hpp b/src/providers/seventv/SeventvEmotes.hpp index 79a8500a..29480163 100644 --- a/src/providers/seventv/SeventvEmotes.hpp +++ b/src/providers/seventv/SeventvEmotes.hpp @@ -108,7 +108,7 @@ public: static void loadChannelEmotes( const std::weak_ptr &channel, const QString &channelId, std::function callback, - bool manualRefresh); + bool manualRefresh, bool cacheHit); /** * Adds an emote to the `map` if it's valid. diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index bb45f246..9b6a71ea 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -327,6 +327,17 @@ void TwitchChannel::refreshBTTVChannelEmotes(bool manualRefresh) return; } + bool cacheHit = readProviderEmotesCache( + this->roomId(), "betterttv", + [this, weak = weakOf(this)](auto jsonDoc) { + if (auto shared = weak.lock()) + { + auto emoteMap = bttv::detail::parseChannelEmotes( + jsonDoc.object(), this->getLocalizedName()); + this->setBttvEmotes(std::make_shared(emoteMap)); + } + }); + BttvEmotes::loadChannel( weakOf(this), this->roomId(), this->getLocalizedName(), [this, weak = weakOf(this)](auto &&emoteMap) { @@ -335,7 +346,7 @@ void TwitchChannel::refreshBTTVChannelEmotes(bool manualRefresh) this->setBttvEmotes(std::make_shared(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(emoteMap)); + }); + FfzEmotes::loadChannel( weakOf(this), this->roomId(), [this, weak = weakOf(this)](auto &&emoteMap) { @@ -376,7 +393,7 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh) std::forward(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(emoteMap)); + }); + SeventvEmotes::loadChannelEmotes( weakOf(this), this->roomId(), [this, weak = weakOf(this)](auto &&emoteMap, @@ -401,7 +427,7 @@ void TwitchChannel::refreshSevenTVChannelEmotes(bool manualRefresh) channelInfo.twitchConnectionIndex; } }, - manualRefresh); + manualRefresh, cacheHit); } void TwitchChannel::setBttvEmotes(std::shared_ptr &&map) diff --git a/src/singletons/Paths.cpp b/src/singletons/Paths.cpp index b315f16d..9185e3d0 100644 --- a/src/singletons/Paths.cpp +++ b/src/singletons/Paths.cpp @@ -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 = diff --git a/src/singletons/Paths.hpp b/src/singletons/Paths.hpp index eabd0693..151db1dc 100644 --- a/src/singletons/Paths.hpp +++ b/src/singletons/Paths.hpp @@ -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 /foo + QString cacheFilePath(const QString &fileName) const; + private: void initAppFilePathHash(); void initCheckPortable(); diff --git a/src/util/Helpers.cpp b/src/util/Helpers.cpp index 8d8ba63b..25ad9f82 100644 --- a/src/util/Helpers.cpp +++ b/src/util/Helpers.cpp @@ -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 #include +#include #include +#include #include +#include +#include +#include #include #include @@ -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 &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 diff --git a/src/util/Helpers.hpp b/src/util/Helpers.hpp index 27c15d7f..0ae95035 100644 --- a/src/util/Helpers.hpp +++ b/src/util/Helpers.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -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 &callback); + } // namespace chatterino