diff --git a/CHANGELOG.md b/CHANGELOG.md index 38f5aacd..43c7786a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Minor: Add categories to the emoji viewer. (#6598) - Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605) - 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: 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) diff --git a/benchmarks/src/RecentMessages.cpp b/benchmarks/src/RecentMessages.cpp index 4888d3e1..6df4e458 100644 --- a/benchmarks/src/RecentMessages.cpp +++ b/benchmarks/src/RecentMessages.cpp @@ -9,6 +9,7 @@ #include "mocks/Logging.hpp" #include "mocks/TwitchIrcServer.hpp" #include "mocks/UserData.hpp" +#include "providers/bttv/BttvBadges.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/ffz/FfzBadges.hpp" @@ -71,6 +72,11 @@ public: return &this->ffzBadges; } + BttvBadges *getBttvBadges() override + { + return &this->bttvBadges; + } + SeventvBadges *getSeventvBadges() override { return &this->seventvBadges; @@ -124,6 +130,7 @@ public: mock::EmptyLinkResolver linkResolver; ChatterinoBadges chatterinoBadges; FfzBadges ffzBadges; + BttvBadges bttvBadges; SeventvBadges seventvBadges; HighlightController highlights; TwitchBadges twitchBadges; diff --git a/docs/lua-meta/globals.lua b/docs/lua-meta/globals.lua index 33445c64..599342f2 100644 --- a/docs/lua-meta/globals.lua +++ b/docs/lua-meta/globals.lua @@ -436,6 +436,7 @@ c2.MessageElementFlag = { BadgeVanity = 0, BadgeChatterino = 0, BadgeSevenTV = 0, + BadgeBttv = 0, BadgeFfz = 0, Badges = 0, ChannelName = 0, @@ -503,6 +504,7 @@ c2.MessageFlag = { EventSub = 0, ModerationAction = 0, InvalidReplyTarget = 0, + WatchStreak = 0, } -- End src/messages/MessageFlag.hpp diff --git a/mocks/include/mocks/EmptyApplication.hpp b/mocks/include/mocks/EmptyApplication.hpp index 808b3429..0c43bf1e 100644 --- a/mocks/include/mocks/EmptyApplication.hpp +++ b/mocks/include/mocks/EmptyApplication.hpp @@ -164,6 +164,12 @@ public: return nullptr; } + BttvBadges *getBttvBadges() override + { + assert(!"getBttvBadges was called without being initialized"); + return nullptr; + } + SeventvBadges *getSeventvBadges() override { assert(!"getSeventvBadges was called without being initialized"); diff --git a/src/Application.cpp b/src/Application.cpp index ca8f2b59..7d2b70d6 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -11,6 +11,7 @@ #include "controllers/ignores/IgnoreController.hpp" #include "controllers/notifications/NotificationController.hpp" #include "controllers/sound/ISoundController.hpp" +#include "providers/bttv/BttvBadges.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/ffz/FfzEmotes.hpp" #include "providers/links/LinkResolver.hpp" @@ -99,7 +100,8 @@ ISoundController *makeSoundController(Settings &settings) BttvLiveUpdates *makeBttvLiveUpdates(Settings &settings) { bool enabled = - settings.enableBTTVLiveUpdates && settings.enableBTTVChannelEmotes; + settings.enableBTTVLiveUpdates && + (settings.enableBTTVChannelEmotes || settings.showBadgesBttv); if (enabled) { @@ -179,6 +181,7 @@ Application::Application(Settings &_settings, const Paths &paths, , highlights(new HighlightController(_settings, this->accounts.get())) , twitch(new TwitchIrcServer) , ffzBadges(new FfzBadges) + , bttvBadges(new BttvBadges) , seventvBadges(new SeventvBadges) , userData(new UserDataController(paths)) , sound(makeSoundController(_settings)) @@ -424,6 +427,14 @@ FfzBadges *Application::getFfzBadges() 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 handles its own locks, so we don't need to assert that this is called in the GUI thread diff --git a/src/Application.hpp b/src/Application.hpp index 1c106549..5d226c5c 100644 --- a/src/Application.hpp +++ b/src/Application.hpp @@ -39,6 +39,7 @@ class Toasts; class IChatterinoBadges; class ChatterinoBadges; class FfzBadges; +class BttvBadges; class SeventvBadges; class ImageUploader; class SeventvAPI; @@ -90,6 +91,7 @@ public: virtual ILogging *getChatLogger() = 0; virtual IChatterinoBadges *getChatterinoBadges() = 0; virtual FfzBadges *getFfzBadges() = 0; + virtual BttvBadges *getBttvBadges() = 0; virtual SeventvBadges *getSeventvBadges() = 0; virtual IUserDataController *getUserData() = 0; virtual ISoundController *getSound() = 0; @@ -162,6 +164,7 @@ private: std::unique_ptr highlights; std::unique_ptr twitch; std::unique_ptr ffzBadges; + std::unique_ptr bttvBadges; std::unique_ptr seventvBadges; std::unique_ptr userData; std::unique_ptr sound; @@ -206,6 +209,7 @@ public: PubSub *getTwitchPubSub() override; ILogging *getChatLogger() override; FfzBadges *getFfzBadges() override; + BttvBadges *getBttvBadges() override; SeventvBadges *getSeventvBadges() override; IUserDataController *getUserData() override; ISoundController *getSound() override; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fbff2511..72b110e4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -353,6 +353,8 @@ set(SOURCE_FILES providers/NetworkConfigurationProvider.cpp providers/NetworkConfigurationProvider.hpp + providers/bttv/BttvBadges.cpp + providers/bttv/BttvBadges.hpp providers/bttv/BttvEmotes.cpp providers/bttv/BttvEmotes.hpp providers/bttv/BttvLiveUpdates.cpp @@ -525,6 +527,8 @@ set(SOURCE_FILES util/AbandonObject.hpp util/AttachToConsole.cpp util/AttachToConsole.hpp + util/BadgeRegistry.cpp + util/BadgeRegistry.hpp util/CancellationToken.hpp util/ChannelHelpers.hpp util/Clipboard.cpp diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index 8077c098..6be351eb 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -17,6 +17,7 @@ #include "messages/MessageColor.hpp" #include "messages/MessageElement.hpp" #include "messages/MessageThread.hpp" +#include "providers/bttv/BttvBadges.hpp" #include "providers/bttv/BttvEmotes.hpp" #include "providers/chatterino/ChatterinoBadges.hpp" #include "providers/colors/ColorProvider.hpp" @@ -1636,6 +1637,7 @@ std::pair MessageBuilder::makeIrcMessage( builder.appendChatterinoBadges(userID); builder.appendFfzBadges(twitchChannel, userID); + builder.appendBttvBadges(userID); builder.appendSeventvBadges(userID); 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(*badge, MessageElementFlag::BadgeBttv); + } +} + void MessageBuilder::appendSeventvBadges(const QString &userID) { if (auto badge = getApp()->getSeventvBadges()->getBadge({userID})) diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 0476f346..76345839 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -315,6 +315,7 @@ private: TwitchChannel *twitchChannel); void appendChatterinoBadges(const QString &userID); void appendFfzBadges(TwitchChannel *twitchChannel, const QString &userID); + void appendBttvBadges(const QString &userID); void appendSeventvBadges(const QString &userID); [[nodiscard]] static bool isIgnored(const QString &originalMessage, diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 2cb0d07c..4423f02e 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -44,7 +44,6 @@ enum class MessageElementFlag : int64_t { EmoteText = (1LL << 5), Emote = EmoteImage | EmoteText, - // unused: (1LL << 6), // unused: (1LL << 7), ChannelPointReward = (1LL << 8), @@ -105,7 +104,11 @@ enum class MessageElementFlag : int64_t { // - 7TV Contributor BadgeSevenTV = (1LL << 36), - // Slot 7: FrankerFaceZ + // Slot 8: BetterTTV + // - BetterTTV Pro + BadgeBttv = (1LL << 6), + + // Slot 9: FrankerFaceZ // - FFZ developer badge // - FFZ bot badge // - FFZ donator badge @@ -113,7 +116,7 @@ enum class MessageElementFlag : int64_t { Badges = BadgeGlobalAuthority | BadgePredictions | BadgeChannelAuthority | BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeSevenTV | - BadgeFfz | BadgeSharedChannel, + BadgeFfz | BadgeSharedChannel | BadgeBttv, ChannelName = (1LL << 20), diff --git a/src/providers/bttv/BttvBadges.cpp b/src/providers/bttv/BttvBadges.cpp new file mode 100644 index 00000000..eec21ca1 --- /dev/null +++ b/src/providers/bttv/BttvBadges.cpp @@ -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(std::move(emote)); +} + +} // namespace chatterino diff --git a/src/providers/bttv/BttvBadges.hpp b/src/providers/bttv/BttvBadges.hpp new file mode 100644 index 00000000..cdd743ad --- /dev/null +++ b/src/providers/bttv/BttvBadges.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "util/BadgeRegistry.hpp" + +#include + +#include + +namespace chatterino { + +struct Emote; +using EmotePtr = std::shared_ptr; + +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 diff --git a/src/providers/bttv/BttvLiveUpdates.cpp b/src/providers/bttv/BttvLiveUpdates.cpp index 172d0ab9..fc758d43 100644 --- a/src/providers/bttv/BttvLiveUpdates.cpp +++ b/src/providers/bttv/BttvLiveUpdates.cpp @@ -24,6 +24,7 @@ public: BttvLiveUpdatesPrivate &operator=(const BttvLiveUpdatesPrivate &&) = delete; std::shared_ptr makeClient(); + std::shared_ptr anyClient(); // Contains all joined Twitch channel-ids std::unordered_set joinedChannels; @@ -50,6 +51,15 @@ std::shared_ptr BttvLiveUpdatesPrivate::makeClient() return std::make_shared(this->parent); } +std::shared_ptr BttvLiveUpdatesPrivate::anyClient() +{ + if (this->clients().empty()) + { + return nullptr; + } + return this->clients().begin()->second; +} + BttvLiveUpdates::BttvLiveUpdates(QString host) : private_(std::make_unique(*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() { this->private_->stop(); diff --git a/src/providers/bttv/BttvLiveUpdates.hpp b/src/providers/bttv/BttvLiveUpdates.hpp index 7c9cd0c6..a5c67441 100644 --- a/src/providers/bttv/BttvLiveUpdates.hpp +++ b/src/providers/bttv/BttvLiveUpdates.hpp @@ -39,6 +39,8 @@ public: */ 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:') * if it's joined. diff --git a/src/providers/bttv/liveupdates/BttvLiveUpdateClient.cpp b/src/providers/bttv/liveupdates/BttvLiveUpdateClient.cpp index 41d517cd..10f255ef 100644 --- a/src/providers/bttv/liveupdates/BttvLiveUpdateClient.cpp +++ b/src/providers/bttv/liveupdates/BttvLiveUpdateClient.cpp @@ -1,11 +1,14 @@ #include "providers/bttv/liveupdates/BttvLiveUpdateClient.hpp" +#include "Application.hpp" +#include "providers/bttv/BttvBadges.hpp" #include "providers/bttv/BttvLiveUpdates.hpp" #include "providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp" #include #include #include +#include namespace chatterino { @@ -67,7 +70,25 @@ void BttvLiveUpdateClient::onMessage(const QByteArray &msg) } 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 { @@ -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 diff --git a/src/providers/bttv/liveupdates/BttvLiveUpdateClient.hpp b/src/providers/bttv/liveupdates/BttvLiveUpdateClient.hpp index 84902cb0..e3b1e06e 100644 --- a/src/providers/bttv/liveupdates/BttvLiveUpdateClient.hpp +++ b/src/providers/bttv/liveupdates/BttvLiveUpdateClient.hpp @@ -15,6 +15,8 @@ public: void onMessage(const QByteArray &msg) /* override */; + void broadcastMe(const QString &channelID, const QString &userID); + private: BttvLiveUpdates &manager; }; diff --git a/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp b/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp index a0a5405e..e0c2c544 100644 --- a/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp +++ b/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.cpp @@ -2,6 +2,8 @@ namespace { +using namespace Qt::Literals; + bool tryParseChannelId(QString &channelId) { if (!channelId.startsWith("twitch:")) @@ -49,4 +51,21 @@ bool BttvLiveUpdateEmoteRemoveMessage::validate() const !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 diff --git a/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp b/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp index afa25ff7..5f8c1b42 100644 --- a/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp +++ b/src/providers/bttv/liveupdates/BttvLiveUpdateMessages.hpp @@ -35,4 +35,14 @@ private: bool badChannelID_; }; +struct BttvLiveUpdateUserUpdateMessage { + BttvLiveUpdateUserUpdateMessage(const QJsonObject &json); + + QString userID; + QJsonObject badgeObject; + + bool validate() const; + bool hasBadge() const; +}; + } // namespace chatterino diff --git a/src/providers/liveupdates/BasicPubSubClient.hpp b/src/providers/liveupdates/BasicPubSubClient.hpp index a5cbed17..6f15b23e 100644 --- a/src/providers/liveupdates/BasicPubSubClient.hpp +++ b/src/providers/liveupdates/BasicPubSubClient.hpp @@ -61,6 +61,11 @@ public: this->ws_.close(); } + void sendText(const QByteArray &data) + { + this->ws_.sendText(data); + } + protected: /** * @return true if this client subscribed to this subscription diff --git a/src/providers/seventv/SeventvBadges.cpp b/src/providers/seventv/SeventvBadges.cpp index e217fa91..583316a0 100644 --- a/src/providers/seventv/SeventvBadges.cpp +++ b/src/providers/seventv/SeventvBadges.cpp @@ -4,74 +4,30 @@ #include "messages/Image.hpp" #include "providers/seventv/SeventvEmotes.hpp" -#include -#include -#include - namespace chatterino { -std::optional SeventvBadges::getBadge(const UserId &id) const +QString SeventvBadges::idForBadge(const QJsonObject &badgeJson) 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; + return badgeJson["id"].toString(); } -void SeventvBadges::assignBadgeToUser(const QString &badgeID, - const UserId &userID) +EmotePtr SeventvBadges::createBadge(const QString &id, + 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{ .name = EmoteName{}, .images = SeventvEmotes::createImageSet(badgeJson, true), .tooltip = Tooltip{badgeJson["tooltip"].toString()}, .homePage = Url{}, - .id = EmoteId{badgeID}, + .id = EmoteId{id}, }; if (emote.images.getImage1()->isEmpty()) { - return; // Bad images + return nullptr; // Bad images } - this->knownBadges_[badgeID] = - std::make_shared(std::move(emote)); + return std::make_shared(std::move(emote)); } } // namespace chatterino diff --git a/src/providers/seventv/SeventvBadges.hpp b/src/providers/seventv/SeventvBadges.hpp index c9d0efad..3398564c 100644 --- a/src/providers/seventv/SeventvBadges.hpp +++ b/src/providers/seventv/SeventvBadges.hpp @@ -1,44 +1,25 @@ #pragma once -#include "common/Aliases.hpp" -#include "util/QStringHash.hpp" +#include "util/BadgeRegistry.hpp" #include #include -#include -#include -#include namespace chatterino { struct Emote; using EmotePtr = std::shared_ptr; -class SeventvBadges +class SeventvBadges : public BadgeRegistry { public: - // Return the badge, if any, that is assigned to the user - std::optional getBadge(const UserId &id) const; + SeventvBadges() = default; - // 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 - void registerBadge(const QJsonObject &badgeJson); - -private: - // Mutex for both `badgeMap_` and `knownBadges_` - mutable std::shared_mutex mutex_; - - // user-id => badge - std::unordered_map badgeMap_; - // badge-id => badge - std::unordered_map knownBadges_; +protected: + QString idForBadge(const QJsonObject &badgeJson) const override; + EmotePtr createBadge(const QString &id, + const QJsonObject &badgeJson) const override; }; } // namespace chatterino diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 57ef24fb..830a88e7 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -831,6 +831,7 @@ void TwitchChannel::sendMessage(const QString &message) bool messageSent = false; this->sendMessageSignal.invoke(this->getName(), parsedMessage, messageSent); + this->updateBttvActivity(); this->updateSevenTVActivity(); if (messageSent) @@ -2256,6 +2257,31 @@ std::optional TwitchChannel::cheerEmote(const QString &string) const 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() { static const QString seventvActivityUrl = diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 62316875..03018490 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -377,6 +377,9 @@ private: /** Joins (subscribes to) a Twitch channel for updates on BTTV. */ void joinBttvChannel() const; + + void updateBttvActivity(); + /** * Indicates an activity to 7TV in this channel for this user. * This is done at most once every 60s. @@ -514,6 +517,8 @@ private: */ QDateTime nextSeventvActivity_; + QDateTime nextBttvActivity_; + /** The platform of the last live emote update ("7TV", "BTTV", "FFZ"). */ QString lastLiveUpdateEmotePlatform_; /** The actor name of the last live emote update. */ diff --git a/src/singletons/Settings.hpp b/src/singletons/Settings.hpp index 79db70ee..ea72f2d8 100644 --- a/src/singletons/Settings.hpp +++ b/src/singletons/Settings.hpp @@ -269,6 +269,7 @@ public: "/appearance/badges/useCustomFfzModeratorBadges", true}; BoolSetting useCustomFfzVipBadges = { "/appearance/badges/useCustomFfzVipBadges", true}; + BoolSetting showBadgesBttv = {"/appearance/badges/bttv", true}; BoolSetting showBadgesSevenTV = {"/appearance/badges/seventv", true}; QSizeSetting lastPopupSize = { "/appearance/lastPopup/size", @@ -386,6 +387,7 @@ public: BoolSetting enableBTTVGlobalEmotes = {"/emotes/bttv/global", true}; BoolSetting enableBTTVChannelEmotes = {"/emotes/bttv/channel", true}; BoolSetting enableBTTVLiveUpdates = {"/emotes/bttv/liveupdates", true}; + BoolSetting sendBTTVActivity = {"/emotes/bttv/sendActivity", true}; BoolSetting enableFFZGlobalEmotes = {"/emotes/ffz/global", true}; BoolSetting enableFFZChannelEmotes = {"/emotes/ffz/channel", true}; BoolSetting enableSevenTVGlobalEmotes = {"/emotes/seventv/global", true}; diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index 02f6d942..a550f288 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -232,6 +232,7 @@ void WindowManager::updateWordTypeMask() flags.set(settings->showBadgesChatterino ? MEF::BadgeChatterino : 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); // username diff --git a/src/util/BadgeRegistry.cpp b/src/util/BadgeRegistry.cpp new file mode 100644 index 00000000..ea9ba1db --- /dev/null +++ b/src/util/BadgeRegistry.cpp @@ -0,0 +1,68 @@ +#include "util/BadgeRegistry.hpp" + +#include "messages/Emote.hpp" + +#include +#include +#include + +namespace chatterino { + +std::optional 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 diff --git a/src/util/BadgeRegistry.hpp b/src/util/BadgeRegistry.hpp new file mode 100644 index 00000000..d62c82aa --- /dev/null +++ b/src/util/BadgeRegistry.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "common/Aliases.hpp" + +#include + +#include + +namespace chatterino { + +struct Emote; +using EmotePtr = std::shared_ptr; + +class BadgeRegistry +{ +public: + virtual ~BadgeRegistry() = default; + + /// Return the badge, if any, that is assigned to the user + std::optional 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 badgeMap_; + /// badge-id => badge + std::unordered_map knownBadges_; +}; + +} // namespace chatterino diff --git a/src/widgets/settingspages/GeneralPage.cpp b/src/widgets/settingspages/GeneralPage.cpp index 4822a6e2..68f2f30e 100644 --- a/src/widgets/settingspages/GeneralPage.cpp +++ b/src/widgets/settingspages/GeneralPage.cpp @@ -659,6 +659,14 @@ void GeneralPage::initLayout(GeneralPageView &layout) s.enableBTTVLiveUpdates) ->addKeywords({"bttv"}) ->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", s.enableFFZGlobalEmotes) @@ -1106,6 +1114,9 @@ void GeneralPage::initLayout(GeneralPageView &layout) ->addKeywords({"seventv"}) ->setTooltip("Badges for 7TV admins, developers, and supporters") ->addTo(layout); + SettingWidget::checkbox("BetterTTV", s.showBadgesBttv) + ->addKeywords({"bttv"}) + ->addTo(layout); layout.addSeparator(); SettingWidget::checkbox("Use custom FrankerFaceZ moderator badges", s.useCustomFfzModeratorBadges) diff --git a/tests/src/Filters.cpp b/tests/src/Filters.cpp index 7347f06d..4627dd5f 100644 --- a/tests/src/Filters.cpp +++ b/tests/src/Filters.cpp @@ -12,6 +12,7 @@ #include "mocks/Logging.hpp" #include "mocks/TwitchIrcServer.hpp" #include "mocks/UserData.hpp" +#include "providers/bttv/BttvBadges.hpp" #include "providers/ffz/FfzBadges.hpp" #include "providers/seventv/SeventvBadges.hpp" #include "providers/twitch/TwitchBadge.hpp" @@ -64,6 +65,11 @@ public: return &this->ffzBadges; } + BttvBadges *getBttvBadges() override + { + return &this->bttvBadges; + } + SeventvBadges *getSeventvBadges() override { return &this->seventvBadges; @@ -86,6 +92,7 @@ public: mock::MockTwitchIrcServer twitch; mock::ChatterinoBadges chatterinoBadges; FfzBadges ffzBadges; + BttvBadges bttvBadges; SeventvBadges seventvBadges; HighlightController highlights; }; diff --git a/tests/src/IrcMessageHandler.cpp b/tests/src/IrcMessageHandler.cpp index 9e56c045..802c55c3 100644 --- a/tests/src/IrcMessageHandler.cpp +++ b/tests/src/IrcMessageHandler.cpp @@ -16,6 +16,7 @@ #include "mocks/Logging.hpp" #include "mocks/TwitchIrcServer.hpp" #include "mocks/UserData.hpp" +#include "providers/bttv/BttvBadges.hpp" #include "providers/ffz/FfzBadges.hpp" #include "providers/seventv/SeventvBadges.hpp" #include "providers/twitch/api/Helix.hpp" @@ -102,6 +103,11 @@ public: return &this->ffzBadges; } + BttvBadges *getBttvBadges() override + { + return &this->bttvBadges; + } + SeventvBadges *getSeventvBadges() override { return &this->seventvBadges; @@ -154,6 +160,7 @@ public: mock::MockTwitchIrcServer twitch; mock::ChatterinoBadges chatterinoBadges; FfzBadges ffzBadges; + BttvBadges bttvBadges; SeventvBadges seventvBadges; HighlightController highlights; BttvEmotes bttvEmotes; diff --git a/tests/src/Plugins.cpp b/tests/src/Plugins.cpp index 0e0fe9f8..5b6d7ec2 100644 --- a/tests/src/Plugins.cpp +++ b/tests/src/Plugins.cpp @@ -931,6 +931,7 @@ TEST_F(PluginTest, MessageElementFlag) )lua"); const char *VALUES = "AlwaysShow=0x2000000," + "BadgeBttv=0x40," "BadgeChannelAuthority=0x8000," "BadgeChatterino=0x40000," "BadgeFfz=0x80000,"