diff --git a/src/controllers/filters/lang/Filter.cpp b/src/controllers/filters/lang/Filter.cpp index 7ae61991a..18890f6d5 100644 --- a/src/controllers/filters/lang/Filter.cpp +++ b/src/controllers/filters/lang/Filter.cpp @@ -24,6 +24,7 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) * List of identifiers: * * author.badges + * author.badge_texts * author.color * author.name * author.no_color @@ -56,11 +57,16 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) QStringList badges; badges.reserve(m->badges.size()); + QStringList badgeTexts; + badgeTexts.reserve(m->badges.size()); for (const auto &e : m->badges) { - badges << e.key_; + badges << e.key; + badgeTexts << e.text; } + qDebug() << "XXX: Badges:" << badges << badgeTexts; + bool watching = !watchingChannel->getName().isEmpty() && watchingChannel->getName().compare( m->channelName, Qt::CaseInsensitive) == 0; @@ -81,6 +87,7 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) } ContextMap vars = { {"author.badges", std::move(badges)}, + {"author.badge_texts", std::move(badgeTexts)}, {"author.color", m->usernameColor}, {"author.name", m->displayName}, {"author.no_color", !m->usernameColor.isValid()}, diff --git a/src/controllers/filters/lang/Filter.hpp b/src/controllers/filters/lang/Filter.hpp index c8afbd769..6911522ef 100644 --- a/src/controllers/filters/lang/Filter.hpp +++ b/src/controllers/filters/lang/Filter.hpp @@ -24,6 +24,7 @@ namespace chatterino::filters { // i.e. if all the variables and operators being used have compatible types. static const QMap MESSAGE_TYPING_CONTEXT = { {"author.badges", Type::StringList}, + {"author.badge_texts", Type::StringList}, {"author.color", Type::Color}, {"author.name", Type::String}, {"author.no_color", Type::Bool}, diff --git a/src/controllers/filters/lang/Tokenizer.hpp b/src/controllers/filters/lang/Tokenizer.hpp index 2fbc5fd95..a830347ae 100644 --- a/src/controllers/filters/lang/Tokenizer.hpp +++ b/src/controllers/filters/lang/Tokenizer.hpp @@ -10,6 +10,7 @@ namespace chatterino::filters { static const QMap validIdentifiersMap = { {"author.badges", "author badges"}, + {"author.badge_texts", "author badge texts"}, {"author.color", "author color"}, {"author.name", "author name"}, {"author.no_color", "author has no color?"}, @@ -35,7 +36,8 @@ static const QMap validIdentifiersMap = { {"flags.restricted", "restricted message?"}, {"flags.monitored", "monitored message?"}, {"message.content", "message text"}, - {"message.length", "message length"}}; + {"message.length", "message length"}, +}; // clang-format off static const QRegularExpression tokenRegex( diff --git a/src/controllers/highlights/HighlightBadge.cpp b/src/controllers/highlights/HighlightBadge.cpp index b4452a22b..06ceb04af 100644 --- a/src/controllers/highlights/HighlightBadge.cpp +++ b/src/controllers/highlights/HighlightBadge.cpp @@ -98,11 +98,11 @@ bool HighlightBadge::compare(const QString &id, const Badge &badge) const if (this->hasVersions_) { auto parts = SharedMessageBuilder::slashKeyValue(id); - return parts.first.compare(badge.key_, Qt::CaseInsensitive) == 0 && - parts.second.compare(badge.value_, Qt::CaseInsensitive) == 0; + return parts.first.compare(badge.key, Qt::CaseInsensitive) == 0 && + parts.second.compare(badge.value, Qt::CaseInsensitive) == 0; } - return id.compare(badge.key_, Qt::CaseInsensitive) == 0; + return id.compare(badge.key, Qt::CaseInsensitive) == 0; } bool HighlightBadge::hasCustomSound() const diff --git a/src/messages/search/BadgePredicate.cpp b/src/messages/search/BadgePredicate.cpp index dc3fbafa8..3f7c16836 100644 --- a/src/messages/search/BadgePredicate.cpp +++ b/src/messages/search/BadgePredicate.cpp @@ -36,7 +36,7 @@ bool BadgePredicate::appliesToImpl(const Message &message) { for (const Badge &badge : message.badges) { - if (badges_.contains(badge.key_, Qt::CaseInsensitive)) + if (badges_.contains(badge.key, Qt::CaseInsensitive)) { return true; } diff --git a/src/messages/search/SubtierPredicate.cpp b/src/messages/search/SubtierPredicate.cpp index 2dc79fc35..12c8f1c95 100644 --- a/src/messages/search/SubtierPredicate.cpp +++ b/src/messages/search/SubtierPredicate.cpp @@ -20,10 +20,10 @@ bool SubtierPredicate::appliesToImpl(const Message &message) { for (const Badge &badge : message.badges) { - if (badge.key_ == "subscriber") + if (badge.key == "subscriber") { const auto &subTier = - badge.value_.length() > 3 ? badge.value_.at(0) : '1'; + badge.value.length() > 3 ? badge.value.at(0) : '1'; return subtiers_.contains(subTier); } diff --git a/src/providers/twitch/TwitchBadge.cpp b/src/providers/twitch/TwitchBadge.cpp index 0a8799927..3d8771454 100644 --- a/src/providers/twitch/TwitchBadge.cpp +++ b/src/providers/twitch/TwitchBadge.cpp @@ -12,30 +12,30 @@ const QSet channelAuthority{"moderator", "vip", "broadcaster"}; const QSet subBadges{"subscriber", "founder"}; Badge::Badge(QString key, QString value) - : key_(std::move(key)) - , value_(std::move(value)) + : key(std::move(key)) + , value(std::move(value)) { - if (globalAuthority.contains(this->key_)) + if (globalAuthority.contains(this->key)) { - this->flag_ = MessageElementFlag::BadgeGlobalAuthority; + this->flag = MessageElementFlag::BadgeGlobalAuthority; } - else if (predictions.contains(this->key_)) + else if (predictions.contains(this->key)) { - this->flag_ = MessageElementFlag::BadgePredictions; + this->flag = MessageElementFlag::BadgePredictions; } - else if (channelAuthority.contains(this->key_)) + else if (channelAuthority.contains(this->key)) { - this->flag_ = MessageElementFlag::BadgeChannelAuthority; + this->flag = MessageElementFlag::BadgeChannelAuthority; } - else if (subBadges.contains(this->key_)) + else if (subBadges.contains(this->key)) { - this->flag_ = MessageElementFlag::BadgeSubscription; + this->flag = MessageElementFlag::BadgeSubscription; } } bool Badge::operator==(const Badge &other) const { - return this->key_ == other.key_ && this->value_ == other.value_; + return this->key == other.key && this->value == other.value; } } // namespace chatterino diff --git a/src/providers/twitch/TwitchBadge.hpp b/src/providers/twitch/TwitchBadge.hpp index f3267687e..e1869e73f 100644 --- a/src/providers/twitch/TwitchBadge.hpp +++ b/src/providers/twitch/TwitchBadge.hpp @@ -4,8 +4,14 @@ #include +#include +#include + namespace chatterino { +struct Emote; +using EmotePtr = std::shared_ptr; + class Badge { public: @@ -13,13 +19,42 @@ public: bool operator==(const Badge &other) const; - // Class members are fetched from both "badges" and "badge-info" tags - // E.g.: "badges": "subscriber/18", "badge-info": "subscriber/22" - QString key_; // subscriber - QString value_; // 18 - //QString info_; // 22 (should be parsed separetly into an std::unordered_map) - MessageElementFlag flag_{ - MessageElementFlag::BadgeVanity}; // badge slot it takes up + /** + * The key of the badge (e.g. subscriber, moderator, chatter-cs-go-2022) + */ + QString key; // subscriber + // + /** + * The value of the badge (e.g. 96 for a subscriber badge, denoting that this should use the 96-month sub badge) + */ + QString value; + + /** + * The text of the badge + * By default, the text is empty & will be filled in separately if text is found + * The text is what will be displayed in the badge's tooltip + */ + QString text; + + /** + * The image of the badge + * Can be nullopt if the badge just doesn't have an image, or if no image has been found set it yet + */ + std::optional image{}; + + /** + * The badge slot this badge takes up + */ + MessageElementFlag flag{MessageElementFlag::BadgeVanity}; }; } // namespace chatterino + +inline QDebug operator<<(QDebug debug, const chatterino::Badge &v) +{ + QDebugStateSaver saver(debug); + debug.nospace() << "(key=" << v.key << ", value=" << v.value + << ", text=" << v.text << ')'; + + return debug; +} diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index 47fcf93f2..038416110 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -157,13 +157,13 @@ namespace { const TwitchChannel *twitchChannel) { if (auto channelBadge = - twitchChannel->twitchBadge(badge.key_, badge.value_)) + twitchChannel->twitchBadge(badge.key, badge.value_)) { return channelBadge; } if (auto globalBadge = - TwitchBadges::instance()->badge(badge.key_, badge.value_)) + TwitchBadges::instance()->badge(badge.key, badge.value_)) { return globalBadge; } @@ -182,20 +182,13 @@ namespace { for (const auto &badge : badges) { - auto badgeEmote = getTwitchBadge(badge, twitchChannel); - if (!badgeEmote) + if (!badge.image) { continue; } - auto tooltip = (*badgeEmote)->tooltip.string; - if (badge.key_ == "bits") - { - const auto &cheerAmount = badge.value_; - tooltip = QString("Twitch cheer %0").arg(cheerAmount); - } - else if (badge.key_ == "moderator" && - getSettings()->useCustomFfzModeratorBadges) + if (badge.key == "moderator" && + getSettings()->useCustomFfzModeratorBadges) { if (auto customModBadge = twitchChannel->ffzCustomModBadge()) { @@ -203,13 +196,12 @@ namespace { ->emplace( *customModBadge, MessageElementFlag::BadgeChannelAuthority) - ->setTooltip((*customModBadge)->tooltip.string); + ->setTooltip(badge.text); // early out, since we have to add a custom badge element here continue; } } - else if (badge.key_ == "vip" && - getSettings()->useCustomFfzVipBadges) + else if (badge.key == "vip" && getSettings()->useCustomFfzVipBadges) { if (auto customVipBadge = twitchChannel->ffzCustomVipBadge()) { @@ -217,49 +209,14 @@ namespace { ->emplace( *customVipBadge, MessageElementFlag::BadgeChannelAuthority) - ->setTooltip((*customVipBadge)->tooltip.string); + ->setTooltip(badge.text); // early out, since we have to add a custom badge element here continue; } } - else if (badge.flag_ == MessageElementFlag::BadgeSubscription) - { - auto badgeInfoIt = badgeInfos.find(badge.key_); - if (badgeInfoIt != badgeInfos.end()) - { - // badge.value_ is 4 chars long if user is subbed on higher tier - // (tier + amount of months with leading zero if less than 100) - // e.g. 3054 - tier 3 4,5-year sub. 2108 - tier 2 9-year sub - const auto &subTier = - badge.value_.length() > 3 ? badge.value_.at(0) : '1'; - const auto &subMonths = badgeInfoIt->second; - tooltip += QString(" (%1%2 months)") - .arg(subTier != '1' - ? QString("Tier %1, ").arg(subTier) - : "") - .arg(subMonths); - } - } - else if (badge.flag_ == MessageElementFlag::BadgePredictions) - { - auto badgeInfoIt = badgeInfos.find(badge.key_); - if (badgeInfoIt != badgeInfos.end()) - { - auto infoValue = badgeInfoIt->second; - auto predictionText = - infoValue - .replace(R"(\s)", " ") // standard IRC escapes - .replace(R"(\:)", ";") - .replace(R"(\\)", R"(\)") - .replace("⸝", ","); // twitch's comma escape - // Careful, the first character is RIGHT LOW PARAPHRASE BRACKET or U+2E1D, which just looks like a comma - tooltip = QString("Predicted %1").arg(predictionText); - } - } - - builder->emplace(*badgeEmote, badge.flag_) - ->setTooltip(tooltip); + builder->emplace(*badge.image, badge.flag) + ->setTooltip(badge.text); } builder->message().badges = badges; @@ -1285,6 +1242,62 @@ void TwitchMessageBuilder::appendTwitchBadges() auto badgeInfos = TwitchMessageBuilder::parseBadgeInfoTag(this->tags); auto badges = TwitchMessageBuilder::parseBadgeTag(this->tags); + for (auto &badge : badges) + { + auto oBadgeEmote = getTwitchBadge(badge, this->twitchChannel); + if (!oBadgeEmote.has_value()) + { + continue; + } + const auto &badgeEmote = oBadgeEmote.value(); + badge.image = badgeEmote; + + if (badge.key == "bits") + { + const auto &cheerAmount = badge.value; + badge.text = QString("Twitch cheer %0").arg(cheerAmount); + } + else if (badge.flag == MessageElementFlag::BadgeSubscription) + { + auto badgeInfoIt = badgeInfos.find(badge.key); + if (badgeInfoIt != badgeInfos.end()) + { + // badge.value_ is 4 chars long if user is subbed on higher tier + // (tier + amount of months with leading zero if less than 100) + // e.g. 3054 - tier 3 4,5-year sub. 2108 - tier 2 9-year sub + const auto &subTier = + badge.value.length() > 3 ? badge.value.at(0) : '1'; + const auto &subMonths = badgeInfoIt->second; + badge.text = + QString("%0 (%1%2 months)") + .arg(badgeEmote->tooltip.string) + .arg(subTier != '1' ? QString("Tier %1, ").arg(subTier) + : "") + .arg(subMonths); + } + } + else if (badge.flag == MessageElementFlag::BadgePredictions) + { + auto badgeInfoIt = badgeInfos.find(badge.key); + if (badgeInfoIt != badgeInfos.end()) + { + auto infoValue = badgeInfoIt->second; + auto predictionText = + infoValue + .replace(R"(\s)", " ") // standard IRC escapes + .replace(R"(\:)", ";") + .replace(R"(\\)", R"(\)") + .replace("⸝", ","); // twitch's comma escape + // Careful, the first character is RIGHT LOW PARAPHRASE BRACKET or U+2E1D, which just looks like a comma + + badge.text = QString("Predicted %1").arg(predictionText); + } + } + else + { + badge.text = badgeEmote->tooltip.string; + } + } appendBadges(this, badges, badgeInfos, this->twitchChannel); } diff --git a/tests/src/Filters.cpp b/tests/src/Filters.cpp index 011c18c2d..544cfd345 100644 --- a/tests/src/Filters.cpp +++ b/tests/src/Filters.cpp @@ -193,7 +193,19 @@ TEST(Filters, Evaluation) {"author.subbed", QVariant(false)}, {"message.content", QVariant("hey there :) 2038-01-19 123 456")}, {"channel.name", QVariant("forsen")}, - {"author.badges", QVariant(QStringList({"moderator", "staff"}))}}; + {"author.badges", + QStringList{ + "moderator", + "staff", + "premium", + }}, + {"author.badge_texts", + QStringList{ + "Moderator", + "Staff", + "Prime Gaming", + }}, + }; // clang-format off std::vector tests @@ -234,6 +246,10 @@ TEST(Filters, Evaluation) {R".(!author.subbed).", QVariant(true)}, {R".(author.color == "#ff0000").", QVariant(true)}, {R".(channel.name == "forsen" && author.badges contains "moderator").", QVariant(true)}, + {R".(author.badges contains "moderator").", QVariant(true)}, + {R".(author.badge_texts contains "Moderator").", QVariant(true)}, + {R".(author.badge_texts contains "Subscriber").", QVariant(false)}, + {R".(author.badge_texts contains "Prime Gaming").", QVariant(true)}, {R".(message.content match {r"(\d\d\d\d)\-(\d\d)\-(\d\d)", 3}).", QVariant("19")}, {R".(message.content match r"HEY THERE").", QVariant(false)}, {R".(message.content match ri"HEY THERE").", QVariant(true)},