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: 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)

View file

@ -42,8 +42,9 @@ std::vector<FfzBadges::Badge> FfzBadges::getUserBadges(const UserId &id)
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);
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();

View file

@ -3,6 +3,7 @@
#include "common/Aliases.hpp"
#include "common/Singleton.hpp"
#include "util/QStringHash.hpp"
#include "util/ThreadGuard.hpp"
#include <QColor>
@ -30,10 +31,9 @@ public:
};
std::vector<Badge> getUserBadges(const UserId &id);
std::optional<Badge> getBadge(int badgeID) const;
private:
std::optional<Badge> 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<int, Badge> badges;
ThreadGuard tgBadges;
};
} // namespace chatterino

View file

@ -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<EmoteMap>())
{
@ -220,6 +247,7 @@ void FfzEmotes::loadChannel(
std::function<void(EmoteMap &&)> emoteCallback,
std::function<void(std::optional<EmotePtr>)> modBadgeCallback,
std::function<void(std::optional<EmotePtr>)> vipBadgeCallback,
std::function<void(FfzChannelBadgeMap &&)> 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)

View file

@ -2,7 +2,9 @@
#include "common/Aliases.hpp"
#include "common/Atomic.hpp"
#include "util/QStringHash.hpp"
#include <boost/unordered/unordered_flat_map.hpp>
#include <QJsonObject>
#include <memory>
@ -15,10 +17,19 @@ using EmotePtr = std::shared_ptr<const Emote>;
class EmoteMap;
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 {
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<void(EmoteMap &&)> emoteCallback,
std::function<void(std::optional<EmotePtr>)> modBadgeCallback,
std::function<void(std::optional<EmotePtr>)> vipBadgeCallback,
std::function<void(FfzChannelBadgeMap &&)> channelBadgesCallback,
bool manualRefresh);
private:

View file

@ -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<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);
}
@ -1707,6 +1716,33 @@ std::optional<EmotePtr> TwitchChannel::twitchBadge(const QString &set,
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
{
return this->ffzCustomModBadge_.get();

View file

@ -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 <boost/circular_buffer/space_optimized.hpp>
#include <boost/signals2.hpp>
@ -200,6 +203,10 @@ public:
std::optional<EmotePtr> ffzCustomVipBadge() const;
std::optional<EmotePtr> twitchBadge(const QString &set,
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
std::optional<CheerEmote> cheerEmote(const QString &string);
@ -393,6 +400,9 @@ protected:
Atomic<std::optional<EmotePtr>> ffzCustomModBadge_;
Atomic<std::optional<EmotePtr>> ffzCustomVipBadge_;
FfzChannelBadgeMap ffzChannelBadges_;
ThreadGuard tgFfzChannelBadges_;
private:
// Badges
UniqueAccess<std::map<QString, std::map<QString, EmotePtr>>>

View file

@ -1447,6 +1447,18 @@ void TwitchMessageBuilder::appendFfzBadges()
this->emplace<FfzBadgeElement>(
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()

View file

@ -1,10 +1,23 @@
#pragma once
#include <boost/container_hash/hash_fwd.hpp>
#include <QHash>
#include <QString>
#include <functional>
namespace boost {
template <>
struct hash<QString> {
std::size_t operator()(QString const &s) const
{
return qHash(s);
}
};
} // namespace boost
namespace std {
#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
struct ThreadGuard {
#ifndef NDEBUG
std::mutex mutex;
std::optional<std::thread::id> threadID;
mutable std::mutex mutex;
mutable std::optional<std::thread::id> threadID;
#endif
inline void guard()
inline void guard() const
{
#ifndef NDEBUG
std::unique_lock lock(this->mutex);