feat: Show FrankerFaceZ channel badges (#5119)

This commit is contained in:
pajlada 2024-02-25 12:18:57 +01:00 committed by GitHub
parent 2815c7b67d
commit 101dc82ea0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 127 additions and 8 deletions

View file

@ -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: 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 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: 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: 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: Add a new completion API for experimental plugins feature. (#5000, #5047)
- Minor: Re-enabled _Restart on crash_ option on Windows. (#5012) - Minor: Re-enabled _Restart on crash_ option on Windows. (#5012)

View file

@ -42,8 +42,9 @@ std::vector<FfzBadges::Badge> FfzBadges::getUserBadges(const UserId &id)
return badges; return badges;
} }
std::optional<FfzBadges::Badge> FfzBadges::getBadge(const int badgeID) std::optional<FfzBadges::Badge> FfzBadges::getBadge(const int badgeID) const
{ {
this->tgBadges.guard();
auto it = this->badges.find(badgeID); auto it = this->badges.find(badgeID);
if (it != this->badges.end()) if (it != this->badges.end())
{ {
@ -62,6 +63,7 @@ void FfzBadges::load()
std::unique_lock lock(this->mutex_); std::unique_lock lock(this->mutex_);
auto jsonRoot = result.parseJson(); auto jsonRoot = result.parseJson();
this->tgBadges.guard();
for (const auto &jsonBadge_ : jsonRoot.value("badges").toArray()) for (const auto &jsonBadge_ : jsonRoot.value("badges").toArray())
{ {
auto jsonBadge = jsonBadge_.toObject(); auto jsonBadge = jsonBadge_.toObject();

View file

@ -3,6 +3,7 @@
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include "common/Singleton.hpp" #include "common/Singleton.hpp"
#include "util/QStringHash.hpp" #include "util/QStringHash.hpp"
#include "util/ThreadGuard.hpp"
#include <QColor> #include <QColor>
@ -30,10 +31,9 @@ public:
}; };
std::vector<Badge> getUserBadges(const UserId &id); std::vector<Badge> getUserBadges(const UserId &id);
std::optional<Badge> getBadge(int badgeID) const;
private: private:
std::optional<Badge> getBadge(int badgeID);
void load(); void load();
std::shared_mutex mutex_; std::shared_mutex mutex_;
@ -43,6 +43,7 @@ private:
// badges points a badge ID to the information about the badge // badges points a badge ID to the information about the badge
std::unordered_map<int, Badge> badges; std::unordered_map<int, Badge> badges;
ThreadGuard tgBadges;
}; };
} // namespace chatterino } // namespace chatterino

View file

@ -169,6 +169,33 @@ EmoteMap ffz::detail::parseChannelEmotes(const QJsonObject &jsonRoot)
return emotes; 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() FfzEmotes::FfzEmotes()
: global_(std::make_shared<EmoteMap>()) : global_(std::make_shared<EmoteMap>())
{ {
@ -220,6 +247,7 @@ void FfzEmotes::loadChannel(
std::function<void(EmoteMap &&)> emoteCallback, std::function<void(EmoteMap &&)> emoteCallback,
std::function<void(std::optional<EmotePtr>)> modBadgeCallback, std::function<void(std::optional<EmotePtr>)> modBadgeCallback,
std::function<void(std::optional<EmotePtr>)> vipBadgeCallback, std::function<void(std::optional<EmotePtr>)> vipBadgeCallback,
std::function<void(FfzChannelBadgeMap &&)> channelBadgesCallback,
bool manualRefresh) bool manualRefresh)
{ {
qCDebug(LOG) << "Reload FFZ Channel Emotes for channel" << channelID; qCDebug(LOG) << "Reload FFZ Channel Emotes for channel" << channelID;
@ -229,8 +257,9 @@ void FfzEmotes::loadChannel(
.timeout(20000) .timeout(20000)
.onSuccess([emoteCallback = std::move(emoteCallback), .onSuccess([emoteCallback = std::move(emoteCallback),
modBadgeCallback = std::move(modBadgeCallback), modBadgeCallback = std::move(modBadgeCallback),
vipBadgeCallback = std::move(vipBadgeCallback), channel, vipBadgeCallback = std::move(vipBadgeCallback),
manualRefresh](const auto &result) { channelBadgesCallback = std::move(channelBadgesCallback),
channel, manualRefresh](const auto &result) {
const auto json = result.parseJson(); const auto json = result.parseJson();
auto emoteMap = parseChannelEmotes(json); auto emoteMap = parseChannelEmotes(json);
@ -238,12 +267,15 @@ void FfzEmotes::loadChannel(
json["room"]["mod_urls"].toObject(), "Moderator"); json["room"]["mod_urls"].toObject(), "Moderator");
auto vipBadge = parseAuthorityBadge( auto vipBadge = parseAuthorityBadge(
json["room"]["vip_badge"].toObject(), "VIP"); json["room"]["vip_badge"].toObject(), "VIP");
auto channelBadges =
parseChannelBadges(json["room"]["user_badge_ids"].toObject());
bool hasEmotes = !emoteMap.empty(); bool hasEmotes = !emoteMap.empty();
emoteCallback(std::move(emoteMap)); emoteCallback(std::move(emoteMap));
modBadgeCallback(std::move(modBadge)); modBadgeCallback(std::move(modBadge));
vipBadgeCallback(std::move(vipBadge)); vipBadgeCallback(std::move(vipBadge));
channelBadgesCallback(std::move(channelBadges));
if (auto shared = channel.lock(); manualRefresh) if (auto shared = channel.lock(); manualRefresh)
{ {
if (hasEmotes) if (hasEmotes)

View file

@ -2,7 +2,9 @@
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include "common/Atomic.hpp" #include "common/Atomic.hpp"
#include "util/QStringHash.hpp"
#include <boost/unordered/unordered_flat_map.hpp>
#include <QJsonObject> #include <QJsonObject>
#include <memory> #include <memory>
@ -15,10 +17,19 @@ using EmotePtr = std::shared_ptr<const Emote>;
class EmoteMap; class EmoteMap;
class Channel; class Channel;
/// Maps a Twitch User ID to a list of badge IDs
using FfzChannelBadgeMap =
boost::unordered::unordered_flat_map<QString, std::vector<int>>;
namespace ffz::detail { namespace ffz::detail {
EmoteMap parseChannelEmotes(const QJsonObject &jsonRoot); 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 } // namespace ffz::detail
class FfzEmotes final class FfzEmotes final
@ -35,6 +46,7 @@ public:
std::function<void(EmoteMap &&)> emoteCallback, std::function<void(EmoteMap &&)> emoteCallback,
std::function<void(std::optional<EmotePtr>)> modBadgeCallback, std::function<void(std::optional<EmotePtr>)> modBadgeCallback,
std::function<void(std::optional<EmotePtr>)> vipBadgeCallback, std::function<void(std::optional<EmotePtr>)> vipBadgeCallback,
std::function<void(FfzChannelBadgeMap &&)> channelBadgesCallback,
bool manualRefresh); bool manualRefresh);
private: private:

View file

@ -18,6 +18,7 @@
#include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/BttvEmotes.hpp"
#include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/bttv/BttvLiveUpdates.hpp"
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" #include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
#include "providers/ffz/FfzBadges.hpp"
#include "providers/ffz/FfzEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp"
#include "providers/recentmessages/Api.hpp" #include "providers/recentmessages/Api.hpp"
#include "providers/seventv/eventapi/Dispatch.hpp" #include "providers/seventv/eventapi/Dispatch.hpp"
@ -333,6 +334,14 @@ void TwitchChannel::refreshFFZChannelEmotes(bool manualRefresh)
std::forward<decltype(vipBadge)>(vipBadge)); std::forward<decltype(vipBadge)>(vipBadge));
} }
}, },
[this, weak = weakOf<Channel>(this)](auto &&channelBadges) {
if (auto shared = weak.lock())
{
this->tgFfzChannelBadges_.guard();
this->ffzChannelBadges_ =
std::forward<decltype(channelBadges)>(channelBadges);
}
},
manualRefresh); manualRefresh);
} }
@ -1707,6 +1716,33 @@ std::optional<EmotePtr> TwitchChannel::twitchBadge(const QString &set,
return std::nullopt; return std::nullopt;
} }
std::vector<FfzBadges::Badge> TwitchChannel::ffzChannelBadges(
const QString &userID) const
{
this->tgFfzChannelBadges_.guard();
auto it = this->ffzChannelBadges_.find(userID);
if (it == this->ffzChannelBadges_.end())
{
return {};
}
std::vector<FfzBadges::Badge> 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<EmotePtr> TwitchChannel::ffzCustomModBadge() const std::optional<EmotePtr> TwitchChannel::ffzCustomModBadge() const
{ {
return this->ffzCustomModBadge_.get(); return this->ffzCustomModBadge_.get();

View file

@ -6,8 +6,11 @@
#include "common/ChannelChatters.hpp" #include "common/ChannelChatters.hpp"
#include "common/Common.hpp" #include "common/Common.hpp"
#include "common/UniqueAccess.hpp" #include "common/UniqueAccess.hpp"
#include "providers/ffz/FfzBadges.hpp"
#include "providers/ffz/FfzEmotes.hpp"
#include "providers/twitch/TwitchEmotes.hpp" #include "providers/twitch/TwitchEmotes.hpp"
#include "util/QStringHash.hpp" #include "util/QStringHash.hpp"
#include "util/ThreadGuard.hpp"
#include <boost/circular_buffer/space_optimized.hpp> #include <boost/circular_buffer/space_optimized.hpp>
#include <boost/signals2.hpp> #include <boost/signals2.hpp>
@ -200,6 +203,10 @@ public:
std::optional<EmotePtr> ffzCustomVipBadge() const; std::optional<EmotePtr> ffzCustomVipBadge() const;
std::optional<EmotePtr> twitchBadge(const QString &set, std::optional<EmotePtr> twitchBadge(const QString &set,
const QString &version) const; const QString &version) const;
/**
* Returns a list of channel-specific FrankerFaceZ badges for the given user
*/
std::vector<FfzBadges::Badge> ffzChannelBadges(const QString &userID) const;
// Cheers // Cheers
std::optional<CheerEmote> cheerEmote(const QString &string); std::optional<CheerEmote> cheerEmote(const QString &string);
@ -393,6 +400,9 @@ protected:
Atomic<std::optional<EmotePtr>> ffzCustomModBadge_; Atomic<std::optional<EmotePtr>> ffzCustomModBadge_;
Atomic<std::optional<EmotePtr>> ffzCustomVipBadge_; Atomic<std::optional<EmotePtr>> ffzCustomVipBadge_;
FfzChannelBadgeMap ffzChannelBadges_;
ThreadGuard tgFfzChannelBadges_;
private: private:
// Badges // Badges
UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>> UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>>

View file

@ -1447,6 +1447,18 @@ void TwitchMessageBuilder::appendFfzBadges()
this->emplace<FfzBadgeElement>( this->emplace<FfzBadgeElement>(
badge.emote, MessageElementFlag::BadgeFfz, badge.color); badge.emote, MessageElementFlag::BadgeFfz, badge.color);
} }
if (this->twitchChannel == nullptr)
{
return;
}
for (const auto &badge :
this->twitchChannel->ffzChannelBadges(this->userId_))
{
this->emplace<FfzBadgeElement>(
badge.emote, MessageElementFlag::BadgeFfz, badge.color);
}
} }
void TwitchMessageBuilder::appendSeventvBadges() void TwitchMessageBuilder::appendSeventvBadges()

View file

@ -1,10 +1,23 @@
#pragma once #pragma once
#include <boost/container_hash/hash_fwd.hpp>
#include <QHash> #include <QHash>
#include <QString> #include <QString>
#include <functional> #include <functional>
namespace boost {
template <>
struct hash<QString> {
std::size_t operator()(QString const &s) const
{
return qHash(s);
}
};
} // namespace boost
namespace std { namespace std {
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)

View file

@ -10,11 +10,11 @@ namespace chatterino {
// Debug-class which asserts if guard of the same object has been called from different threads // Debug-class which asserts if guard of the same object has been called from different threads
struct ThreadGuard { struct ThreadGuard {
#ifndef NDEBUG #ifndef NDEBUG
std::mutex mutex; mutable std::mutex mutex;
std::optional<std::thread::id> threadID; mutable std::optional<std::thread::id> threadID;
#endif #endif
inline void guard() inline void guard() const
{ {
#ifndef NDEBUG #ifndef NDEBUG
std::unique_lock lock(this->mutex); std::unique_lock lock(this->mutex);