diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b96d2dc4..38e61eccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Minor: All sound capabilities can now be disabled by setting your "Sound backend" setting to "Null" and restarting Chatterino. (#4978) - Minor: Add an option to use new experimental smarter emote completion. (#4987) - Minor: Add `--safe-mode` command line option that can be used for troubleshooting when Chatterino is misbehaving or is misconfigured. It disables hiding the settings button & prevents plugins from loading. (#4985) +- Minor: Added support for FrankerFaceZ channel badges. These can be configured at https://www.frankerfacez.com/channel/mine - right now only supporting bot badges for your chat bots. (#5119) - Minor: Updated the flatpakref link included with nightly builds to point to up-to-date flathub-beta builds. (#5008) - Minor: Add a new completion API for experimental plugins feature. (#5000, #5047) - Minor: Re-enabled _Restart on crash_ option on Windows. (#5012) diff --git a/src/providers/ffz/FfzBadges.cpp b/src/providers/ffz/FfzBadges.cpp index 557481835..3c7a98604 100644 --- a/src/providers/ffz/FfzBadges.cpp +++ b/src/providers/ffz/FfzBadges.cpp @@ -42,8 +42,9 @@ std::vector FfzBadges::getUserBadges(const UserId &id) return badges; } -std::optional FfzBadges::getBadge(const int badgeID) +std::optional FfzBadges::getBadge(const int badgeID) const { + this->tgBadges.guard(); auto it = this->badges.find(badgeID); if (it != this->badges.end()) { @@ -62,6 +63,7 @@ void FfzBadges::load() std::unique_lock lock(this->mutex_); auto jsonRoot = result.parseJson(); + this->tgBadges.guard(); for (const auto &jsonBadge_ : jsonRoot.value("badges").toArray()) { auto jsonBadge = jsonBadge_.toObject(); diff --git a/src/providers/ffz/FfzBadges.hpp b/src/providers/ffz/FfzBadges.hpp index a2fde9c8a..8d021624a 100644 --- a/src/providers/ffz/FfzBadges.hpp +++ b/src/providers/ffz/FfzBadges.hpp @@ -3,6 +3,7 @@ #include "common/Aliases.hpp" #include "common/Singleton.hpp" #include "util/QStringHash.hpp" +#include "util/ThreadGuard.hpp" #include @@ -30,10 +31,9 @@ public: }; std::vector getUserBadges(const UserId &id); + std::optional getBadge(int badgeID) const; private: - std::optional getBadge(int badgeID); - void load(); std::shared_mutex mutex_; @@ -43,6 +43,7 @@ private: // badges points a badge ID to the information about the badge std::unordered_map badges; + ThreadGuard tgBadges; }; } // namespace chatterino diff --git a/src/providers/ffz/FfzEmotes.cpp b/src/providers/ffz/FfzEmotes.cpp index dfc047535..7f34cfeda 100644 --- a/src/providers/ffz/FfzEmotes.cpp +++ b/src/providers/ffz/FfzEmotes.cpp @@ -169,6 +169,33 @@ EmoteMap ffz::detail::parseChannelEmotes(const QJsonObject &jsonRoot) return emotes; } +FfzChannelBadgeMap ffz::detail::parseChannelBadges(const QJsonObject &badgeRoot) +{ + FfzChannelBadgeMap channelBadges; + + for (auto it = badgeRoot.begin(); it != badgeRoot.end(); ++it) + { + const auto badgeID = it.key().toInt(); + const auto &jsonUserIDs = it.value().toArray(); + for (const auto &jsonUserID : jsonUserIDs) + { + // NOTE: The Twitch User IDs come through as ints right now, the code below + // tries to parse them as strings first since that's how we treat them anyway. + if (jsonUserID.isString()) + { + channelBadges[jsonUserID.toString()].emplace_back(badgeID); + } + else + { + channelBadges[QString::number(jsonUserID.toInt())].emplace_back( + badgeID); + } + } + } + + return channelBadges; +} + FfzEmotes::FfzEmotes() : global_(std::make_shared()) { @@ -220,6 +247,7 @@ void FfzEmotes::loadChannel( std::function emoteCallback, std::function)> modBadgeCallback, std::function)> vipBadgeCallback, + std::function channelBadgesCallback, bool manualRefresh) { qCDebug(LOG) << "Reload FFZ Channel Emotes for channel" << channelID; @@ -229,8 +257,9 @@ void FfzEmotes::loadChannel( .timeout(20000) .onSuccess([emoteCallback = std::move(emoteCallback), modBadgeCallback = std::move(modBadgeCallback), - vipBadgeCallback = std::move(vipBadgeCallback), channel, - manualRefresh](const auto &result) { + vipBadgeCallback = std::move(vipBadgeCallback), + channelBadgesCallback = std::move(channelBadgesCallback), + channel, manualRefresh](const auto &result) { const auto json = result.parseJson(); auto emoteMap = parseChannelEmotes(json); @@ -238,12 +267,15 @@ void FfzEmotes::loadChannel( json["room"]["mod_urls"].toObject(), "Moderator"); auto vipBadge = parseAuthorityBadge( json["room"]["vip_badge"].toObject(), "VIP"); + auto channelBadges = + parseChannelBadges(json["room"]["user_badge_ids"].toObject()); bool hasEmotes = !emoteMap.empty(); emoteCallback(std::move(emoteMap)); modBadgeCallback(std::move(modBadge)); vipBadgeCallback(std::move(vipBadge)); + channelBadgesCallback(std::move(channelBadges)); if (auto shared = channel.lock(); manualRefresh) { if (hasEmotes) diff --git a/src/providers/ffz/FfzEmotes.hpp b/src/providers/ffz/FfzEmotes.hpp index 4b80789c6..7d639d56f 100644 --- a/src/providers/ffz/FfzEmotes.hpp +++ b/src/providers/ffz/FfzEmotes.hpp @@ -2,7 +2,9 @@ #include "common/Aliases.hpp" #include "common/Atomic.hpp" +#include "util/QStringHash.hpp" +#include #include #include @@ -15,10 +17,19 @@ using EmotePtr = std::shared_ptr; class EmoteMap; class Channel; +/// Maps a Twitch User ID to a list of badge IDs +using FfzChannelBadgeMap = + boost::unordered::unordered_flat_map>; + namespace ffz::detail { EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot); + /** + * Parse the `user_badge_ids` into a map of User IDs -> Badge IDs + */ + FfzChannelBadgeMap parseChannelBadges(const QJsonObject &badgeRoot); + } // namespace ffz::detail class FfzEmotes final @@ -35,6 +46,7 @@ public: std::function emoteCallback, std::function)> modBadgeCallback, std::function)> vipBadgeCallback, + std::function channelBadgesCallback, bool manualRefresh); private: diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 05e48b9dc..162f9aebf 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -18,6 +18,7 @@ #include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" +#include "providers/ffz/FfzBadges.hpp" #include "providers/ffz/FfzEmotes.hpp" #include "providers/recentmessages/Api.hpp" #include "providers/seventv/eventapi/Dispatch.hpp" @@ -333,6 +334,14 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh) std::forward(vipBadge)); } }, + [this, weak = weakOf(this)](auto &&channelBadges) { + if (auto shared = weak.lock()) + { + this->tgFfzChannelBadges_.guard(); + this->ffzChannelBadges_ = + std::forward(channelBadges); + } + }, manualRefresh); } @@ -1707,6 +1716,33 @@ std::optional TwitchChannel::twitchBadge(const QString &set, return std::nullopt; } +std::vector TwitchChannel::ffzChannelBadges( + const QString &userID) const +{ + this->tgFfzChannelBadges_.guard(); + + auto it = this->ffzChannelBadges_.find(userID); + if (it == this->ffzChannelBadges_.end()) + { + return {}; + } + + std::vector badges; + + const auto *ffzBadges = getIApp()->getFfzBadges(); + + for (const auto &badgeID : it->second) + { + auto badge = ffzBadges->getBadge(badgeID); + if (badge.has_value()) + { + badges.emplace_back(*badge); + } + } + + return badges; +} + std::optional TwitchChannel::ffzCustomModBadge() const { return this->ffzCustomModBadge_.get(); diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 6fc7b0433..2add54302 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -6,8 +6,11 @@ #include "common/ChannelChatters.hpp" #include "common/Common.hpp" #include "common/UniqueAccess.hpp" +#include "providers/ffz/FfzBadges.hpp" +#include "providers/ffz/FfzEmotes.hpp" #include "providers/twitch/TwitchEmotes.hpp" #include "util/QStringHash.hpp" +#include "util/ThreadGuard.hpp" #include #include @@ -200,6 +203,10 @@ public: std::optional ffzCustomVipBadge() const; std::optional twitchBadge(const QString &set, const QString &version) const; + /** + * Returns a list of channel-specific FrankerFaceZ badges for the given user + */ + std::vector ffzChannelBadges(const QString &userID) const; // Cheers std::optional cheerEmote(const QString &string); @@ -393,6 +400,9 @@ protected: Atomic> ffzCustomModBadge_; Atomic> ffzCustomVipBadge_; + FfzChannelBadgeMap ffzChannelBadges_; + ThreadGuard tgFfzChannelBadges_; + private: // Badges UniqueAccess>> diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index e333d155a..8f416b8ee 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -1447,6 +1447,18 @@ void TwitchMessageBuilder::appendFfzBadges() this->emplace( badge.emote, MessageElementFlag::BadgeFfz, badge.color); } + + if (this->twitchChannel == nullptr) + { + return; + } + + for (const auto &badge : + this->twitchChannel->ffzChannelBadges(this->userId_)) + { + this->emplace( + badge.emote, MessageElementFlag::BadgeFfz, badge.color); + } } void TwitchMessageBuilder::appendSeventvBadges() diff --git a/src/util/QStringHash.hpp b/src/util/QStringHash.hpp index 73148b814..eb4efe2f0 100644 --- a/src/util/QStringHash.hpp +++ b/src/util/QStringHash.hpp @@ -1,10 +1,23 @@ #pragma once +#include #include #include #include +namespace boost { + +template <> +struct hash { + std::size_t operator()(QString const &s) const + { + return qHash(s); + } +}; + +} // namespace boost + namespace std { #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) diff --git a/src/util/ThreadGuard.hpp b/src/util/ThreadGuard.hpp index 6f272ac42..00748033a 100644 --- a/src/util/ThreadGuard.hpp +++ b/src/util/ThreadGuard.hpp @@ -10,11 +10,11 @@ namespace chatterino { // Debug-class which asserts if guard of the same object has been called from different threads struct ThreadGuard { #ifndef NDEBUG - std::mutex mutex; - std::optional threadID; + mutable std::mutex mutex; + mutable std::optional threadID; #endif - inline void guard() + inline void guard() const { #ifndef NDEBUG std::unique_lock lock(this->mutex);