feat: show BTTV Pro badges (#6625)

Reviewed-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
nerix
2025-12-07 15:15:58 +01:00
committed by GitHub
parent ab8e0751b1
commit 0aff6fbe83
31 changed files with 394 additions and 82 deletions
+1
View File
@@ -18,6 +18,7 @@
- Minor: Add categories to the emoji viewer. (#6598) - Minor: Add categories to the emoji viewer. (#6598)
- Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605) - Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605)
- Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, and `/lockprediction` commands. (#6583, #6612) - Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, and `/lockprediction` commands. (#6583, #6612)
- Minor: Added support for BetterTTV Pro subscriber badges. (#6625)
- Bugfix: Expose the "Extra extension IDs" setting on non-Windows systems too. (#6509) - Bugfix: Expose the "Extra extension IDs" setting on non-Windows systems too. (#6509)
- Bugfix: Fixed some commands and filters not working as expected in seach popups. (#6539) - Bugfix: Fixed some commands and filters not working as expected in seach popups. (#6539)
- Bugfix: Fixed settings occasionally not opening when clicking on "Manage Accounts" in the account switcher. (#6543) - Bugfix: Fixed settings occasionally not opening when clicking on "Manage Accounts" in the account switcher. (#6543)
+7
View File
@@ -9,6 +9,7 @@
#include "mocks/Logging.hpp" #include "mocks/Logging.hpp"
#include "mocks/TwitchIrcServer.hpp" #include "mocks/TwitchIrcServer.hpp"
#include "mocks/UserData.hpp" #include "mocks/UserData.hpp"
#include "providers/bttv/BttvBadges.hpp"
#include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/BttvEmotes.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/ffz/FfzBadges.hpp" #include "providers/ffz/FfzBadges.hpp"
@@ -71,6 +72,11 @@ public:
return &this->ffzBadges; return &this->ffzBadges;
} }
BttvBadges *getBttvBadges() override
{
return &this->bttvBadges;
}
SeventvBadges *getSeventvBadges() override SeventvBadges *getSeventvBadges() override
{ {
return &this->seventvBadges; return &this->seventvBadges;
@@ -124,6 +130,7 @@ public:
mock::EmptyLinkResolver linkResolver; mock::EmptyLinkResolver linkResolver;
ChatterinoBadges chatterinoBadges; ChatterinoBadges chatterinoBadges;
FfzBadges ffzBadges; FfzBadges ffzBadges;
BttvBadges bttvBadges;
SeventvBadges seventvBadges; SeventvBadges seventvBadges;
HighlightController highlights; HighlightController highlights;
TwitchBadges twitchBadges; TwitchBadges twitchBadges;
+2
View File
@@ -436,6 +436,7 @@ c2.MessageElementFlag = {
BadgeVanity = 0, BadgeVanity = 0,
BadgeChatterino = 0, BadgeChatterino = 0,
BadgeSevenTV = 0, BadgeSevenTV = 0,
BadgeBttv = 0,
BadgeFfz = 0, BadgeFfz = 0,
Badges = 0, Badges = 0,
ChannelName = 0, ChannelName = 0,
@@ -503,6 +504,7 @@ c2.MessageFlag = {
EventSub = 0, EventSub = 0,
ModerationAction = 0, ModerationAction = 0,
InvalidReplyTarget = 0, InvalidReplyTarget = 0,
WatchStreak = 0,
} }
-- End src/messages/MessageFlag.hpp -- End src/messages/MessageFlag.hpp
+6
View File
@@ -164,6 +164,12 @@ public:
return nullptr; return nullptr;
} }
BttvBadges *getBttvBadges() override
{
assert(!"getBttvBadges was called without being initialized");
return nullptr;
}
SeventvBadges *getSeventvBadges() override SeventvBadges *getSeventvBadges() override
{ {
assert(!"getSeventvBadges was called without being initialized"); assert(!"getSeventvBadges was called without being initialized");
+12 -1
View File
@@ -11,6 +11,7 @@
#include "controllers/ignores/IgnoreController.hpp" #include "controllers/ignores/IgnoreController.hpp"
#include "controllers/notifications/NotificationController.hpp" #include "controllers/notifications/NotificationController.hpp"
#include "controllers/sound/ISoundController.hpp" #include "controllers/sound/ISoundController.hpp"
#include "providers/bttv/BttvBadges.hpp"
#include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/BttvEmotes.hpp"
#include "providers/ffz/FfzEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp"
#include "providers/links/LinkResolver.hpp" #include "providers/links/LinkResolver.hpp"
@@ -99,7 +100,8 @@ ISoundController *makeSoundController(Settings &settings)
BttvLiveUpdates *makeBttvLiveUpdates(Settings &settings) BttvLiveUpdates *makeBttvLiveUpdates(Settings &settings)
{ {
bool enabled = bool enabled =
settings.enableBTTVLiveUpdates && settings.enableBTTVChannelEmotes; settings.enableBTTVLiveUpdates &&
(settings.enableBTTVChannelEmotes || settings.showBadgesBttv);
if (enabled) if (enabled)
{ {
@@ -179,6 +181,7 @@ Application::Application(Settings &_settings, const Paths &paths,
, highlights(new HighlightController(_settings, this->accounts.get())) , highlights(new HighlightController(_settings, this->accounts.get()))
, twitch(new TwitchIrcServer) , twitch(new TwitchIrcServer)
, ffzBadges(new FfzBadges) , ffzBadges(new FfzBadges)
, bttvBadges(new BttvBadges)
, seventvBadges(new SeventvBadges) , seventvBadges(new SeventvBadges)
, userData(new UserDataController(paths)) , userData(new UserDataController(paths))
, sound(makeSoundController(_settings)) , sound(makeSoundController(_settings))
@@ -424,6 +427,14 @@ FfzBadges *Application::getFfzBadges()
return this->ffzBadges.get(); return this->ffzBadges.get();
} }
BttvBadges *Application::getBttvBadges()
{
// BttvBadges handles its own locks, so we don't need to assert that this is called in the GUI thread
assert(this->bttvBadges);
return this->bttvBadges.get();
}
SeventvBadges *Application::getSeventvBadges() SeventvBadges *Application::getSeventvBadges()
{ {
// SeventvBadges handles its own locks, so we don't need to assert that this is called in the GUI thread // SeventvBadges handles its own locks, so we don't need to assert that this is called in the GUI thread
+4
View File
@@ -39,6 +39,7 @@ class Toasts;
class IChatterinoBadges; class IChatterinoBadges;
class ChatterinoBadges; class ChatterinoBadges;
class FfzBadges; class FfzBadges;
class BttvBadges;
class SeventvBadges; class SeventvBadges;
class ImageUploader; class ImageUploader;
class SeventvAPI; class SeventvAPI;
@@ -90,6 +91,7 @@ public:
virtual ILogging *getChatLogger() = 0; virtual ILogging *getChatLogger() = 0;
virtual IChatterinoBadges *getChatterinoBadges() = 0; virtual IChatterinoBadges *getChatterinoBadges() = 0;
virtual FfzBadges *getFfzBadges() = 0; virtual FfzBadges *getFfzBadges() = 0;
virtual BttvBadges *getBttvBadges() = 0;
virtual SeventvBadges *getSeventvBadges() = 0; virtual SeventvBadges *getSeventvBadges() = 0;
virtual IUserDataController *getUserData() = 0; virtual IUserDataController *getUserData() = 0;
virtual ISoundController *getSound() = 0; virtual ISoundController *getSound() = 0;
@@ -162,6 +164,7 @@ private:
std::unique_ptr<HighlightController> highlights; std::unique_ptr<HighlightController> highlights;
std::unique_ptr<TwitchIrcServer> twitch; std::unique_ptr<TwitchIrcServer> twitch;
std::unique_ptr<FfzBadges> ffzBadges; std::unique_ptr<FfzBadges> ffzBadges;
std::unique_ptr<BttvBadges> bttvBadges;
std::unique_ptr<SeventvBadges> seventvBadges; std::unique_ptr<SeventvBadges> seventvBadges;
std::unique_ptr<UserDataController> userData; std::unique_ptr<UserDataController> userData;
std::unique_ptr<ISoundController> sound; std::unique_ptr<ISoundController> sound;
@@ -206,6 +209,7 @@ public:
PubSub *getTwitchPubSub() override; PubSub *getTwitchPubSub() override;
ILogging *getChatLogger() override; ILogging *getChatLogger() override;
FfzBadges *getFfzBadges() override; FfzBadges *getFfzBadges() override;
BttvBadges *getBttvBadges() override;
SeventvBadges *getSeventvBadges() override; SeventvBadges *getSeventvBadges() override;
IUserDataController *getUserData() override; IUserDataController *getUserData() override;
ISoundController *getSound() override; ISoundController *getSound() override;
+4
View File
@@ -353,6 +353,8 @@ set(SOURCE_FILES
providers/NetworkConfigurationProvider.cpp providers/NetworkConfigurationProvider.cpp
providers/NetworkConfigurationProvider.hpp providers/NetworkConfigurationProvider.hpp
providers/bttv/BttvBadges.cpp
providers/bttv/BttvBadges.hpp
providers/bttv/BttvEmotes.cpp providers/bttv/BttvEmotes.cpp
providers/bttv/BttvEmotes.hpp providers/bttv/BttvEmotes.hpp
providers/bttv/BttvLiveUpdates.cpp providers/bttv/BttvLiveUpdates.cpp
@@ -525,6 +527,8 @@ set(SOURCE_FILES
util/AbandonObject.hpp util/AbandonObject.hpp
util/AttachToConsole.cpp util/AttachToConsole.cpp
util/AttachToConsole.hpp util/AttachToConsole.hpp
util/BadgeRegistry.cpp
util/BadgeRegistry.hpp
util/CancellationToken.hpp util/CancellationToken.hpp
util/ChannelHelpers.hpp util/ChannelHelpers.hpp
util/Clipboard.cpp util/Clipboard.cpp
+10
View File
@@ -17,6 +17,7 @@
#include "messages/MessageColor.hpp" #include "messages/MessageColor.hpp"
#include "messages/MessageElement.hpp" #include "messages/MessageElement.hpp"
#include "messages/MessageThread.hpp" #include "messages/MessageThread.hpp"
#include "providers/bttv/BttvBadges.hpp"
#include "providers/bttv/BttvEmotes.hpp" #include "providers/bttv/BttvEmotes.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/colors/ColorProvider.hpp" #include "providers/colors/ColorProvider.hpp"
@@ -1636,6 +1637,7 @@ std::pair<MessagePtrMut, HighlightAlert> MessageBuilder::makeIrcMessage(
builder.appendChatterinoBadges(userID); builder.appendChatterinoBadges(userID);
builder.appendFfzBadges(twitchChannel, userID); builder.appendFfzBadges(twitchChannel, userID);
builder.appendBttvBadges(userID);
builder.appendSeventvBadges(userID); builder.appendSeventvBadges(userID);
builder.appendUsername(tags, args); builder.appendUsername(tags, args);
@@ -2445,6 +2447,14 @@ void MessageBuilder::appendFfzBadges(TwitchChannel *twitchChannel,
} }
} }
void MessageBuilder::appendBttvBadges(const QString &userID)
{
if (auto badge = getApp()->getBttvBadges()->getBadge({userID}))
{
this->emplace<BadgeElement>(*badge, MessageElementFlag::BadgeBttv);
}
}
void MessageBuilder::appendSeventvBadges(const QString &userID) void MessageBuilder::appendSeventvBadges(const QString &userID)
{ {
if (auto badge = getApp()->getSeventvBadges()->getBadge({userID})) if (auto badge = getApp()->getSeventvBadges()->getBadge({userID}))
+1
View File
@@ -315,6 +315,7 @@ private:
TwitchChannel *twitchChannel); TwitchChannel *twitchChannel);
void appendChatterinoBadges(const QString &userID); void appendChatterinoBadges(const QString &userID);
void appendFfzBadges(TwitchChannel *twitchChannel, const QString &userID); void appendFfzBadges(TwitchChannel *twitchChannel, const QString &userID);
void appendBttvBadges(const QString &userID);
void appendSeventvBadges(const QString &userID); void appendSeventvBadges(const QString &userID);
[[nodiscard]] static bool isIgnored(const QString &originalMessage, [[nodiscard]] static bool isIgnored(const QString &originalMessage,
+6 -3
View File
@@ -44,7 +44,6 @@ enum class MessageElementFlag : int64_t {
EmoteText = (1LL << 5), EmoteText = (1LL << 5),
Emote = EmoteImage | EmoteText, Emote = EmoteImage | EmoteText,
// unused: (1LL << 6),
// unused: (1LL << 7), // unused: (1LL << 7),
ChannelPointReward = (1LL << 8), ChannelPointReward = (1LL << 8),
@@ -105,7 +104,11 @@ enum class MessageElementFlag : int64_t {
// - 7TV Contributor // - 7TV Contributor
BadgeSevenTV = (1LL << 36), BadgeSevenTV = (1LL << 36),
// Slot 7: FrankerFaceZ // Slot 8: BetterTTV
// - BetterTTV Pro
BadgeBttv = (1LL << 6),
// Slot 9: FrankerFaceZ
// - FFZ developer badge // - FFZ developer badge
// - FFZ bot badge // - FFZ bot badge
// - FFZ donator badge // - FFZ donator badge
@@ -113,7 +116,7 @@ enum class MessageElementFlag : int64_t {
Badges = BadgeGlobalAuthority | BadgePredictions | BadgeChannelAuthority | Badges = BadgeGlobalAuthority | BadgePredictions | BadgeChannelAuthority |
BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeSevenTV | BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeSevenTV |
BadgeFfz | BadgeSharedChannel, BadgeFfz | BadgeSharedChannel | BadgeBttv,
ChannelName = (1LL << 20), ChannelName = (1LL << 20),
+27
View File
@@ -0,0 +1,27 @@
#include "providers/bttv/BttvBadges.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
namespace chatterino {
QString BttvBadges::idForBadge(const QJsonObject &badgeJson) const
{
return badgeJson["url"].toString();
}
EmotePtr BttvBadges::createBadge(const QString &id,
const QJsonObject & /* badgeJson */) const
{
auto emote = Emote{
.name = EmoteName{},
.images = ImageSet(Image::fromUrl(Url{id}, 18.0 / 72.0)),
.tooltip = Tooltip{"BTTV Pro"},
.homePage = Url{},
.id = EmoteId{id},
};
return std::make_shared<const Emote>(std::move(emote));
}
} // namespace chatterino
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "util/BadgeRegistry.hpp"
#include <QJsonObject>
#include <memory>
namespace chatterino {
struct Emote;
using EmotePtr = std::shared_ptr<const Emote>;
class BttvBadges : public BadgeRegistry
{
public:
BttvBadges() = default;
protected:
QString idForBadge(const QJsonObject &badgeJson) const override;
EmotePtr createBadge(const QString &id,
const QJsonObject &badgeJson) const override;
};
} // namespace chatterino
+20
View File
@@ -24,6 +24,7 @@ public:
BttvLiveUpdatesPrivate &operator=(const BttvLiveUpdatesPrivate &&) = delete; BttvLiveUpdatesPrivate &operator=(const BttvLiveUpdatesPrivate &&) = delete;
std::shared_ptr<BttvLiveUpdateClient> makeClient(); std::shared_ptr<BttvLiveUpdateClient> makeClient();
std::shared_ptr<BttvLiveUpdateClient> anyClient();
// Contains all joined Twitch channel-ids // Contains all joined Twitch channel-ids
std::unordered_set<QString> joinedChannels; std::unordered_set<QString> joinedChannels;
@@ -50,6 +51,15 @@ std::shared_ptr<BttvLiveUpdateClient> BttvLiveUpdatesPrivate::makeClient()
return std::make_shared<BttvLiveUpdateClient>(this->parent); return std::make_shared<BttvLiveUpdateClient>(this->parent);
} }
std::shared_ptr<BttvLiveUpdateClient> BttvLiveUpdatesPrivate::anyClient()
{
if (this->clients().empty())
{
return nullptr;
}
return this->clients().begin()->second;
}
BttvLiveUpdates::BttvLiveUpdates(QString host) BttvLiveUpdates::BttvLiveUpdates(QString host)
: private_(std::make_unique<BttvLiveUpdatesPrivate>(*this, std::move(host))) : private_(std::make_unique<BttvLiveUpdatesPrivate>(*this, std::move(host)))
{ {
@@ -77,6 +87,16 @@ void BttvLiveUpdates::partChannel(const QString &id)
} }
} }
void BttvLiveUpdates::broadcastMe(const QString &channelID,
const QString &userID)
{
auto client = this->private_->anyClient();
if (client)
{
client->broadcastMe(channelID, userID);
}
}
void BttvLiveUpdates::stop() void BttvLiveUpdates::stop()
{ {
this->private_->stop(); this->private_->stop();
+2
View File
@@ -39,6 +39,8 @@ public:
*/ */
void joinChannel(const QString &channelID, const QString &userID); void joinChannel(const QString &channelID, const QString &userID);
void broadcastMe(const QString &channelID, const QString &userID);
/** /**
* Parts a twitch channel by its id (without any prefix like 'twitch:') * Parts a twitch channel by its id (without any prefix like 'twitch:')
* if it's joined. * if it's joined.
@@ -1,11 +1,14 @@
#include "providers/bttv/liveupdates/BttvLiveUpdateClient.hpp" #include "providers/bttv/liveupdates/BttvLiveUpdateClient.hpp"
#include "Application.hpp"
#include "providers/bttv/BttvBadges.hpp"
#include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/bttv/BttvLiveUpdates.hpp"
#include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" #include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp"
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QJsonValue> #include <QJsonValue>
#include <QStringBuilder>
namespace chatterino { namespace chatterino {
@@ -67,7 +70,25 @@ void BttvLiveUpdateClient::onMessage(const QByteArray &msg)
} }
else if (eventType == "lookup_user") else if (eventType == "lookup_user")
{ {
// ignored auto message = BttvLiveUpdateUserUpdateMessage(eventData);
if (!message.validate())
{
qCDebug(chatterinoBttv) << "Invalid user update message" << json;
return;
}
if (!message.hasBadge())
{
return;
}
auto *app = tryGetApp();
if (app)
{
auto *bttvBadges = app->getBttvBadges();
auto badgeID = bttvBadges->registerBadge(message.badgeObject);
bttvBadges->assignBadgeToUser(badgeID, UserId{message.userID});
}
} }
else else
{ {
@@ -75,4 +96,21 @@ void BttvLiveUpdateClient::onMessage(const QByteArray &msg)
} }
} }
void BttvLiveUpdateClient::broadcastMe(const QString &channelID,
const QString &userID)
{
QJsonObject obj{
{"name", "broadcast_me"},
{"data",
QJsonObject{
{"provider", "twitch"},
{"providerId", userID},
{"channel", QString(u"twitch:" % channelID)},
}},
};
QJsonDocument doc(obj);
this->sendText(doc.toJson(QJsonDocument::Compact));
}
} // namespace chatterino } // namespace chatterino
@@ -15,6 +15,8 @@ public:
void onMessage(const QByteArray &msg) /* override */; void onMessage(const QByteArray &msg) /* override */;
void broadcastMe(const QString &channelID, const QString &userID);
private: private:
BttvLiveUpdates &manager; BttvLiveUpdates &manager;
}; };
@@ -2,6 +2,8 @@
namespace { namespace {
using namespace Qt::Literals;
bool tryParseChannelId(QString &channelId) bool tryParseChannelId(QString &channelId)
{ {
if (!channelId.startsWith("twitch:")) if (!channelId.startsWith("twitch:"))
@@ -49,4 +51,21 @@ bool BttvLiveUpdateEmoteRemoveMessage::validate() const
!this->channelID.isEmpty(); !this->channelID.isEmpty();
} }
BttvLiveUpdateUserUpdateMessage::BttvLiveUpdateUserUpdateMessage(
const QJsonObject &json)
: userID(json["providerId"_L1].toString())
, badgeObject(json["badge"_L1].toObject())
{
}
bool BttvLiveUpdateUserUpdateMessage::validate() const
{
return !this->userID.isEmpty();
}
bool BttvLiveUpdateUserUpdateMessage::hasBadge() const
{
return !this->badgeObject.isEmpty();
}
} // namespace chatterino } // namespace chatterino
@@ -35,4 +35,14 @@ private:
bool badChannelID_; bool badChannelID_;
}; };
struct BttvLiveUpdateUserUpdateMessage {
BttvLiveUpdateUserUpdateMessage(const QJsonObject &json);
QString userID;
QJsonObject badgeObject;
bool validate() const;
bool hasBadge() const;
};
} // namespace chatterino } // namespace chatterino
@@ -61,6 +61,11 @@ public:
this->ws_.close(); this->ws_.close();
} }
void sendText(const QByteArray &data)
{
this->ws_.sendText(data);
}
protected: protected:
/** /**
* @return true if this client subscribed to this subscription * @return true if this client subscribed to this subscription
+7 -51
View File
@@ -4,74 +4,30 @@
#include "messages/Image.hpp" #include "messages/Image.hpp"
#include "providers/seventv/SeventvEmotes.hpp" #include "providers/seventv/SeventvEmotes.hpp"
#include <QJsonArray>
#include <QUrl>
#include <QUrlQuery>
namespace chatterino { namespace chatterino {
std::optional<EmotePtr> SeventvBadges::getBadge(const UserId &id) const QString SeventvBadges::idForBadge(const QJsonObject &badgeJson) const
{ {
std::shared_lock lock(this->mutex_); return badgeJson["id"].toString();
auto it = this->badgeMap_.find(id.string);
if (it != this->badgeMap_.end())
{
return it->second;
}
return std::nullopt;
} }
void SeventvBadges::assignBadgeToUser(const QString &badgeID, EmotePtr SeventvBadges::createBadge(const QString &id,
const UserId &userID) const QJsonObject &badgeJson) const
{ {
const std::unique_lock lock(this->mutex_);
const auto badgeIt = this->knownBadges_.find(badgeID);
if (badgeIt != this->knownBadges_.end())
{
this->badgeMap_[userID.string] = badgeIt->second;
}
}
void SeventvBadges::clearBadgeFromUser(const QString &badgeID,
const UserId &userID)
{
const std::unique_lock lock(this->mutex_);
const auto it = this->badgeMap_.find(userID.string);
if (it != this->badgeMap_.end() && it->second->id.string == badgeID)
{
this->badgeMap_.erase(userID.string);
}
}
void SeventvBadges::registerBadge(const QJsonObject &badgeJson)
{
const auto badgeID = badgeJson["id"].toString();
const std::unique_lock lock(this->mutex_);
if (this->knownBadges_.find(badgeID) != this->knownBadges_.end())
{
return;
}
auto emote = Emote{ auto emote = Emote{
.name = EmoteName{}, .name = EmoteName{},
.images = SeventvEmotes::createImageSet(badgeJson, true), .images = SeventvEmotes::createImageSet(badgeJson, true),
.tooltip = Tooltip{badgeJson["tooltip"].toString()}, .tooltip = Tooltip{badgeJson["tooltip"].toString()},
.homePage = Url{}, .homePage = Url{},
.id = EmoteId{badgeID}, .id = EmoteId{id},
}; };
if (emote.images.getImage1()->isEmpty()) if (emote.images.getImage1()->isEmpty())
{ {
return; // Bad images return nullptr; // Bad images
} }
this->knownBadges_[badgeID] = return std::make_shared<const Emote>(std::move(emote));
std::make_shared<const Emote>(std::move(emote));
} }
} // namespace chatterino } // namespace chatterino
+7 -26
View File
@@ -1,44 +1,25 @@
#pragma once #pragma once
#include "common/Aliases.hpp" #include "util/BadgeRegistry.hpp"
#include "util/QStringHash.hpp"
#include <QJsonObject> #include <QJsonObject>
#include <memory> #include <memory>
#include <optional>
#include <shared_mutex>
#include <unordered_map>
namespace chatterino { namespace chatterino {
struct Emote; struct Emote;
using EmotePtr = std::shared_ptr<const Emote>; using EmotePtr = std::shared_ptr<const Emote>;
class SeventvBadges class SeventvBadges : public BadgeRegistry
{ {
public: public:
// Return the badge, if any, that is assigned to the user SeventvBadges() = default;
std::optional<EmotePtr> getBadge(const UserId &id) const;
// Assign the given badge to the user protected:
void assignBadgeToUser(const QString &badgeID, const UserId &userID); QString idForBadge(const QJsonObject &badgeJson) const override;
EmotePtr createBadge(const QString &id,
// Remove the given badge from the user const QJsonObject &badgeJson) const override;
void clearBadgeFromUser(const QString &badgeID, const UserId &userID);
// Register a new known badge
// The json object will contain all information about the badge, like its ID & its images
void registerBadge(const QJsonObject &badgeJson);
private:
// Mutex for both `badgeMap_` and `knownBadges_`
mutable std::shared_mutex mutex_;
// user-id => badge
std::unordered_map<QString, EmotePtr> badgeMap_;
// badge-id => badge
std::unordered_map<QString, EmotePtr> knownBadges_;
}; };
} // namespace chatterino } // namespace chatterino
+26
View File
@@ -831,6 +831,7 @@ void TwitchChannel::sendMessage(const QString &message)
bool messageSent = false; bool messageSent = false;
this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent); this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent);
this->updateBttvActivity();
this->updateSevenTVActivity(); this->updateSevenTVActivity();
if (messageSent) if (messageSent)
@@ -2256,6 +2257,31 @@ std::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string) const
return std::nullopt; return std::nullopt;
} }
void TwitchChannel::updateBttvActivity()
{
if (!getApp()->getBttvLiveUpdates())
{
return;
}
auto now = QDateTime::currentDateTimeUtc();
if (this->nextBttvActivity_.isValid() && now < this->nextBttvActivity_)
{
return;
}
this->nextBttvActivity_ = now.addSecs(60);
qCDebug(chatterinoBttv) << "Sending activity in" << this->getName();
auto acc = getApp()->getAccounts()->twitch.getCurrent();
if (acc->isAnon())
{
return;
}
getApp()->getBttvLiveUpdates()->broadcastMe(this->roomId(),
acc->getUserId());
}
void TwitchChannel::updateSevenTVActivity() void TwitchChannel::updateSevenTVActivity()
{ {
static const QString seventvActivityUrl = static const QString seventvActivityUrl =
+5
View File
@@ -377,6 +377,9 @@ private:
/** Joins (subscribes to) a Twitch channel for updates on BTTV. */ /** Joins (subscribes to) a Twitch channel for updates on BTTV. */
void joinBttvChannel() const; void joinBttvChannel() const;
void updateBttvActivity();
/** /**
* Indicates an activity to 7TV in this channel for this user. * Indicates an activity to 7TV in this channel for this user.
* This is done at most once every 60s. * This is done at most once every 60s.
@@ -514,6 +517,8 @@ private:
*/ */
QDateTime nextSeventvActivity_; QDateTime nextSeventvActivity_;
QDateTime nextBttvActivity_;
/** The platform of the last live emote update ("7TV", "BTTV", "FFZ"). */ /** The platform of the last live emote update ("7TV", "BTTV", "FFZ"). */
QString lastLiveUpdateEmotePlatform_; QString lastLiveUpdateEmotePlatform_;
/** The actor name of the last live emote update. */ /** The actor name of the last live emote update. */
+2
View File
@@ -269,6 +269,7 @@ public:
"/appearance/badges/useCustomFfzModeratorBadges", true}; "/appearance/badges/useCustomFfzModeratorBadges", true};
BoolSetting useCustomFfzVipBadges = { BoolSetting useCustomFfzVipBadges = {
"/appearance/badges/useCustomFfzVipBadges", true}; "/appearance/badges/useCustomFfzVipBadges", true};
BoolSetting showBadgesBttv = {"/appearance/badges/bttv", true};
BoolSetting showBadgesSevenTV = {"/appearance/badges/seventv", true}; BoolSetting showBadgesSevenTV = {"/appearance/badges/seventv", true};
QSizeSetting lastPopupSize = { QSizeSetting lastPopupSize = {
"/appearance/lastPopup/size", "/appearance/lastPopup/size",
@@ -386,6 +387,7 @@ public:
BoolSetting enableBTTVGlobalEmotes = {"/emotes/bttv/global", true}; BoolSetting enableBTTVGlobalEmotes = {"/emotes/bttv/global", true};
BoolSetting enableBTTVChannelEmotes = {"/emotes/bttv/channel", true}; BoolSetting enableBTTVChannelEmotes = {"/emotes/bttv/channel", true};
BoolSetting enableBTTVLiveUpdates = {"/emotes/bttv/liveupdates", true}; BoolSetting enableBTTVLiveUpdates = {"/emotes/bttv/liveupdates", true};
BoolSetting sendBTTVActivity = {"/emotes/bttv/sendActivity", true};
BoolSetting enableFFZGlobalEmotes = {"/emotes/ffz/global", true}; BoolSetting enableFFZGlobalEmotes = {"/emotes/ffz/global", true};
BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true}; BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true};
BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true}; BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true};
+1
View File
@@ -232,6 +232,7 @@ void WindowManager::updateWordTypeMask()
flags.set(settings->showBadgesChatterino ? MEF::BadgeChatterino flags.set(settings->showBadgesChatterino ? MEF::BadgeChatterino
: MEF::None); : MEF::None);
flags.set(settings->showBadgesFfz ? MEF::BadgeFfz : MEF::None); flags.set(settings->showBadgesFfz ? MEF::BadgeFfz : MEF::None);
flags.set(settings->showBadgesBttv ? MEF::BadgeBttv : MEF::None);
flags.set(settings->showBadgesSevenTV ? MEF::BadgeSevenTV : MEF::None); flags.set(settings->showBadgesSevenTV ? MEF::BadgeSevenTV : MEF::None);
// username // username
+68
View File
@@ -0,0 +1,68 @@
#include "util/BadgeRegistry.hpp"
#include "messages/Emote.hpp"
#include <QJsonArray>
#include <QUrl>
#include <QUrlQuery>
namespace chatterino {
std::optional<EmotePtr> BadgeRegistry::getBadge(const UserId &id) const
{
std::shared_lock lock(this->mutex_);
auto it = this->badgeMap_.find(id.string);
if (it != this->badgeMap_.end())
{
return it->second;
}
return std::nullopt;
}
void BadgeRegistry::assignBadgeToUser(const QString &badgeID,
const UserId &userID)
{
const std::unique_lock lock(this->mutex_);
const auto badgeIt = this->knownBadges_.find(badgeID);
if (badgeIt != this->knownBadges_.end())
{
this->badgeMap_[userID.string] = badgeIt->second;
}
}
void BadgeRegistry::clearBadgeFromUser(const QString &badgeID,
const UserId &userID)
{
const std::unique_lock lock(this->mutex_);
const auto it = this->badgeMap_.find(userID.string);
if (it != this->badgeMap_.end() && it->second->id.string == badgeID)
{
this->badgeMap_.erase(userID.string);
}
}
QString BadgeRegistry::registerBadge(const QJsonObject &badgeJson)
{
const auto badgeID = this->idForBadge(badgeJson);
const std::unique_lock lock(this->mutex_);
if (this->knownBadges_.contains(badgeID))
{
return badgeID;
}
auto emote = this->createBadge(badgeID, badgeJson);
if (!emote)
{
return badgeID;
}
this->knownBadges_[badgeID] = std::move(emote);
return badgeID;
}
} // namespace chatterino
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include "common/Aliases.hpp"
#include <QJsonObject>
#include <shared_mutex>
namespace chatterino {
struct Emote;
using EmotePtr = std::shared_ptr<const Emote>;
class BadgeRegistry
{
public:
virtual ~BadgeRegistry() = default;
/// Return the badge, if any, that is assigned to the user
std::optional<EmotePtr> getBadge(const UserId &id) const;
/// Assign the given badge to the user
void assignBadgeToUser(const QString &badgeID, const UserId &userID);
/// Remove the given badge from the user
void clearBadgeFromUser(const QString &badgeID, const UserId &userID);
/// Register a new known badge
/// The json object will contain all information about the badge, like its ID & its images
/// @returns The badge's ID
QString registerBadge(const QJsonObject &badgeJson);
protected:
BadgeRegistry() = default;
virtual QString idForBadge(const QJsonObject &badgeJson) const = 0;
virtual EmotePtr createBadge(const QString &id,
const QJsonObject &badgeJson) const = 0;
private:
/// Mutex for both `badgeMap_` and `knownBadges_`
mutable std::shared_mutex mutex_;
/// user-id => badge
std::unordered_map<QString, EmotePtr> badgeMap_;
/// badge-id => badge
std::unordered_map<QString, EmotePtr> knownBadges_;
};
} // namespace chatterino
+11
View File
@@ -659,6 +659,14 @@ void GeneralPage::initLayout(GeneralPageView &layout)
s.enableBTTVLiveUpdates) s.enableBTTVLiveUpdates)
->addKeywords({"bttv"}) ->addKeywords({"bttv"})
->addTo(layout); ->addTo(layout);
SettingWidget::checkbox("Send activity to BetterTTV", s.sendBTTVActivity)
->setTooltip(
"When enabled, Chatterino will signal an activity to BetterTTV "
"when you send a chat message. This is used for badges, "
" and personal emotes. When disabled, no activity "
"is sent and others won't see your cosmetics.")
->addKeywords({"bttv"})
->addTo(layout);
SettingWidget::checkbox("Show FrankerFaceZ global emotes", SettingWidget::checkbox("Show FrankerFaceZ global emotes",
s.enableFFZGlobalEmotes) s.enableFFZGlobalEmotes)
@@ -1106,6 +1114,9 @@ void GeneralPage::initLayout(GeneralPageView &layout)
->addKeywords({"seventv"}) ->addKeywords({"seventv"})
->setTooltip("Badges for 7TV admins, developers, and supporters") ->setTooltip("Badges for 7TV admins, developers, and supporters")
->addTo(layout); ->addTo(layout);
SettingWidget::checkbox("BetterTTV", s.showBadgesBttv)
->addKeywords({"bttv"})
->addTo(layout);
layout.addSeparator(); layout.addSeparator();
SettingWidget::checkbox("Use custom FrankerFaceZ moderator badges", SettingWidget::checkbox("Use custom FrankerFaceZ moderator badges",
s.useCustomFfzModeratorBadges) s.useCustomFfzModeratorBadges)
+7
View File
@@ -12,6 +12,7 @@
#include "mocks/Logging.hpp" #include "mocks/Logging.hpp"
#include "mocks/TwitchIrcServer.hpp" #include "mocks/TwitchIrcServer.hpp"
#include "mocks/UserData.hpp" #include "mocks/UserData.hpp"
#include "providers/bttv/BttvBadges.hpp"
#include "providers/ffz/FfzBadges.hpp" #include "providers/ffz/FfzBadges.hpp"
#include "providers/seventv/SeventvBadges.hpp" #include "providers/seventv/SeventvBadges.hpp"
#include "providers/twitch/TwitchBadge.hpp" #include "providers/twitch/TwitchBadge.hpp"
@@ -64,6 +65,11 @@ public:
return &this->ffzBadges; return &this->ffzBadges;
} }
BttvBadges *getBttvBadges() override
{
return &this->bttvBadges;
}
SeventvBadges *getSeventvBadges() override SeventvBadges *getSeventvBadges() override
{ {
return &this->seventvBadges; return &this->seventvBadges;
@@ -86,6 +92,7 @@ public:
mock::MockTwitchIrcServer twitch; mock::MockTwitchIrcServer twitch;
mock::ChatterinoBadges chatterinoBadges; mock::ChatterinoBadges chatterinoBadges;
FfzBadges ffzBadges; FfzBadges ffzBadges;
BttvBadges bttvBadges;
SeventvBadges seventvBadges; SeventvBadges seventvBadges;
HighlightController highlights; HighlightController highlights;
}; };
+7
View File
@@ -16,6 +16,7 @@
#include "mocks/Logging.hpp" #include "mocks/Logging.hpp"
#include "mocks/TwitchIrcServer.hpp" #include "mocks/TwitchIrcServer.hpp"
#include "mocks/UserData.hpp" #include "mocks/UserData.hpp"
#include "providers/bttv/BttvBadges.hpp"
#include "providers/ffz/FfzBadges.hpp" #include "providers/ffz/FfzBadges.hpp"
#include "providers/seventv/SeventvBadges.hpp" #include "providers/seventv/SeventvBadges.hpp"
#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Helix.hpp"
@@ -102,6 +103,11 @@ public:
return &this->ffzBadges; return &this->ffzBadges;
} }
BttvBadges *getBttvBadges() override
{
return &this->bttvBadges;
}
SeventvBadges *getSeventvBadges() override SeventvBadges *getSeventvBadges() override
{ {
return &this->seventvBadges; return &this->seventvBadges;
@@ -154,6 +160,7 @@ public:
mock::MockTwitchIrcServer twitch; mock::MockTwitchIrcServer twitch;
mock::ChatterinoBadges chatterinoBadges; mock::ChatterinoBadges chatterinoBadges;
FfzBadges ffzBadges; FfzBadges ffzBadges;
BttvBadges bttvBadges;
SeventvBadges seventvBadges; SeventvBadges seventvBadges;
HighlightController highlights; HighlightController highlights;
BttvEmotes bttvEmotes; BttvEmotes bttvEmotes;
+1
View File
@@ -931,6 +931,7 @@ TEST_F(PluginTest, MessageElementFlag)
)lua"); )lua");
const char *VALUES = "AlwaysShow=0x2000000," const char *VALUES = "AlwaysShow=0x2000000,"
"BadgeBttv=0x40,"
"BadgeChannelAuthority=0x8000," "BadgeChannelAuthority=0x8000,"
"BadgeChatterino=0x40000," "BadgeChatterino=0x40000,"
"BadgeFfz=0x80000," "BadgeFfz=0x80000,"