Corrected the way we parse comma-separated "list tags" in PRIVMSGs (#3771)

tl;dr: we now split by slash only its first occurrence instead of every occurrence.
This commit is contained in:
Kasia 2022-05-28 11:55:48 +02:00 committed by GitHub
parent 6ef3ecc952
commit 7d0023cf73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 228 additions and 102 deletions

View file

@ -1,5 +1,6 @@
#include "HighlightBadge.hpp"
#include "messages/SharedMessageBuilder.hpp"
#include "singletons/Resources.hpp"
namespace chatterino {
@ -86,21 +87,12 @@ bool HighlightBadge::compare(const QString &id, const Badge &badge) const
{
if (this->hasVersions_)
{
auto parts = id.split("/");
if (parts.size() == 2)
{
return parts.at(0).compare(badge.key_, Qt::CaseInsensitive) == 0 &&
parts.at(1).compare(badge.value_, Qt::CaseInsensitive) == 0;
}
else
{
return parts.at(0).compare(badge.key_, Qt::CaseInsensitive) == 0;
}
}
else
{
return id.compare(badge.key_, Qt::CaseInsensitive) == 0;
auto parts = SharedMessageBuilder::slashKeyValue(id);
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;
}
bool HighlightBadge::hasCustomSound() const

View file

@ -2,6 +2,7 @@
#include "common/FlagsEnum.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include "util/QStringHash.hpp"
#include "widgets/helper/ScrollbarHighlight.hpp"
#include <QTime>
@ -65,7 +66,7 @@ struct Message : boost::noncopyable {
QColor usernameColor;
QDateTime serverReceivedTime;
std::vector<Badge> badges;
std::map<QString, QString> badgeInfos;
std::unordered_map<QString, QString> badgeInfos;
std::shared_ptr<QColor> highlightColor;
uint32_t count = 1;
std::vector<std::unique_ptr<MessageElement>> elements;

View file

@ -4,7 +4,6 @@
#include "common/QLogging.hpp"
#include "controllers/ignores/IgnoreController.hpp"
#include "controllers/ignores/IgnorePhrase.hpp"
#include "messages/Message.hpp"
#include "messages/MessageElement.hpp"
#include "singletons/Settings.hpp"
#include "singletons/WindowManager.hpp"
@ -36,33 +35,6 @@ namespace {
}
}
QStringList parseTagList(const QVariantMap &tags, const QString &key)
{
auto iterator = tags.find(key);
if (iterator == tags.end())
return QStringList{};
return iterator.value().toString().split(',', Qt::SkipEmptyParts);
}
std::vector<Badge> parseBadges(const QVariantMap &tags)
{
std::vector<Badge> badges;
for (QString badge : parseTagList(tags, "badges"))
{
QStringList parts = badge.split('/');
if (parts.size() != 2)
{
continue;
}
badges.emplace_back(parts[0], parts[1]);
}
return badges;
}
} // namespace
SharedMessageBuilder::SharedMessageBuilder(
@ -103,6 +75,46 @@ void SharedMessageBuilder::parse()
this->message().flags.set(MessageFlag::Collapsed);
}
// "foo/bar/baz,tri/hard" can be a valid badge-info tag
// In that case, valid map content should be 'split by slash' only once:
// {"foo": "bar/baz", "tri": "hard"}
std::pair<QString, QString> SharedMessageBuilder::slashKeyValue(
const QString &kvStr)
{
return {
// part before first slash (index 0 of section)
kvStr.section('/', 0, 0),
// part after first slash (index 1 of section)
kvStr.section('/', 1, -1),
};
}
std::vector<Badge> SharedMessageBuilder::parseBadgeTag(const QVariantMap &tags)
{
std::vector<Badge> b;
auto badgesIt = tags.constFind("badges");
if (badgesIt == tags.end())
{
return b;
}
auto badges = badgesIt.value().toString().split(',', Qt::SkipEmptyParts);
for (const QString &badge : badges)
{
if (!badge.contains('/'))
{
continue;
}
auto pair = SharedMessageBuilder::slashKeyValue(badge);
b.emplace_back(Badge{pair.first, pair.second});
}
return b;
}
bool SharedMessageBuilder::isIgnored() const
{
return isIgnoredMessage({
@ -332,7 +344,7 @@ void SharedMessageBuilder::parseHighlights()
}
// Highlight because of badge
auto badges = parseBadges(this->tags);
auto badges = this->parseBadgeTag(this->tags);
auto badgeHighlights = getCSettings().highlightedBadges.readOnly();
bool badgeHighlightSet = false;
for (const HighlightBadge &highlight : *badgeHighlights)

View file

@ -3,6 +3,7 @@
#include "common/Aliases.hpp"
#include "common/Outcome.hpp"
#include "messages/MessageColor.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include <IrcMessage>
#include <QColor>
@ -32,6 +33,11 @@ public:
virtual void triggerHighlights();
virtual MessagePtr build() = 0;
static std::pair<QString, QString> slashKeyValue(const QString &kvStr);
// Parses "badges" tag which contains a comma separated list of key-value elements
static std::vector<Badge> parseBadgeTag(const QVariantMap &tags);
protected:
virtual void parse();

View file

@ -33,4 +33,9 @@ Badge::Badge(QString key, QString value)
}
}
bool Badge::operator==(const Badge &other) const
{
return this->key_ == other.key_ && this->value_ == other.value_;
}
} // namespace chatterino

View file

@ -11,9 +11,13 @@ class Badge
public:
Badge(QString key, QString value);
QString key_; // e.g. bits
QString value_; // e.g. 100
QString extraValue_{}; // e.g. 5 (the number of months subscribed)
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
};

View file

@ -7,6 +7,7 @@
#include "messages/Message.hpp"
#include "providers/chatterino/ChatterinoBadges.hpp"
#include "providers/ffz/FfzBadges.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include "providers/twitch/TwitchBadges.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
@ -47,55 +48,6 @@ const QSet<QString> zeroWidthEmotes{
namespace chatterino {
namespace {
QStringList parseTagList(const QVariantMap &tags, const QString &key)
{
auto iterator = tags.find(key);
if (iterator == tags.end())
return QStringList{};
return iterator.value().toString().split(',', Qt::SkipEmptyParts);
}
std::map<QString, QString> parseBadgeInfos(const QVariantMap &tags)
{
std::map<QString, QString> badgeInfos;
for (QString badgeInfo : parseTagList(tags, "badge-info"))
{
QStringList parts = badgeInfo.split('/');
if (parts.size() != 2)
{
continue;
}
badgeInfos.emplace(parts[0], parts[1]);
}
return badgeInfos;
}
std::vector<Badge> parseBadges(const QVariantMap &tags)
{
std::vector<Badge> badges;
for (QString badge : parseTagList(tags, "badges"))
{
QStringList parts = badge.split('/');
if (parts.size() != 2)
{
continue;
}
badges.emplace_back(parts[0], parts[1]);
}
return badges;
}
} // namespace
TwitchMessageBuilder::TwitchMessageBuilder(
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
const MessageParseArgs &_args)
@ -1033,6 +985,25 @@ boost::optional<EmotePtr> TwitchMessageBuilder::getTwitchBadge(
return boost::none;
}
std::unordered_map<QString, QString> TwitchMessageBuilder::parseBadgeInfoTag(
const QVariantMap &tags)
{
std::unordered_map<QString, QString> infoMap;
auto infoIt = tags.constFind("badge-info");
if (infoIt == tags.end())
return infoMap;
auto info = infoIt.value().toString().split(',', Qt::SkipEmptyParts);
for (const QString &badge : info)
{
infoMap.emplace(SharedMessageBuilder::slashKeyValue(badge));
}
return infoMap;
}
void TwitchMessageBuilder::appendTwitchBadges()
{
if (this->twitchChannel == nullptr)
@ -1040,8 +1011,8 @@ void TwitchMessageBuilder::appendTwitchBadges()
return;
}
auto badgeInfos = parseBadgeInfos(this->tags);
auto badges = parseBadges(this->tags);
auto badgeInfos = TwitchMessageBuilder::parseBadgeInfoTag(this->tags);
auto badges = this->parseBadgeTag(this->tags);
for (const auto &badge : badges)
{
@ -1091,7 +1062,7 @@ void TwitchMessageBuilder::appendTwitchBadges()
// (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_.front() : '1';
badge.value_.length() > 3 ? badge.value_.at(0) : '1';
const auto &subMonths = badgeInfoIt->second;
tooltip +=
QString(" (%1%2 months)")
@ -1107,9 +1078,9 @@ void TwitchMessageBuilder::appendTwitchBadges()
{
auto predictionText =
badgeInfoIt->second
.replace("\\s", " ") // standard IRC escapes
.replace("\\:", ";")
.replace("\\\\", "\\")
.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

View file

@ -68,6 +68,10 @@ public:
Channel *channel,
MessageBuilder *builder);
// Shares some common logic from SharedMessageBuilder::parseBadgeTag
static std::unordered_map<QString, QString> parseBadgeInfoTag(
const QVariantMap &tags);
private:
void parseUsernameColor() override;
void parseUsername() override;

View file

@ -17,6 +17,7 @@ set(test_SOURCES
${CMAKE_CURRENT_LIST_DIR}/src/UtilTwitch.cpp
${CMAKE_CURRENT_LIST_DIR}/src/IrcHelpers.cpp
${CMAKE_CURRENT_LIST_DIR}/src/TwitchPubSubClient.cpp
${CMAKE_CURRENT_LIST_DIR}/src/TwitchMessageBuilder.cpp
# Add your new file above this line!
)

View file

@ -0,0 +1,130 @@
#include "providers/twitch/TwitchMessageBuilder.hpp"
#include "common/Channel.hpp"
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include "ircconnection.h"
#include <gtest/gtest.h>
#include <QDebug>
#include <QString>
#include <unordered_map>
#include <vector>
using namespace chatterino;
TEST(TwitchMessageBuilder, CommaSeparatedListTagParsing)
{
struct TestCase {
QString input;
std::pair<QString, QString> expectedOutput;
};
std::vector<TestCase> testCases{
{
"broadcaster/1",
{"broadcaster", "1"},
},
{
"predictions/foo/bar/baz",
{"predictions", "foo/bar/baz"},
},
{
"test/",
{"test", ""},
},
{
"/",
{"", ""},
},
{
"/value",
{"", "value"},
},
{
"",
{"", ""},
},
};
for (const auto &test : testCases)
{
auto output = TwitchMessageBuilder::slashKeyValue(test.input);
EXPECT_EQ(output, test.expectedOutput)
<< "Input " << test.input.toStdString() << " failed";
}
}
TEST(TwitchMessageBuilder, BadgeInfoParsing)
{
struct TestCase {
QByteArray input;
std::unordered_map<QString, QString> expectedBadgeInfo;
std::vector<Badge> expectedBadges;
};
std::vector<TestCase> testCases{
{
R"(@badge-info=predictions/<<<<<<\sHEAD[15A⸝asdf/test;badges=predictions/pink-2;client-nonce=9dbb88e516edf4efb055c011f91ea0cf;color=#FF4500;display-name=もっと頑張って;emotes=;first-msg=0;flags=;id=feb00b12-4ec5-4f77-9160-667de463dab1;mod=0;room-id=99631238;subscriber=0;tmi-sent-ts=1653494874297;turbo=0;user-id=648946956;user-type= :zniksbot!zniksbot@zniksbot.tmi.twitch.tv PRIVMSG #zneix :-tags")",
{
{"predictions", R"(<<<<<<\sHEAD[15A⸝asdf/test)"},
},
{
Badge{"predictions", "pink-2"},
},
},
{
R"(@badge-info=predictions/<<<<<<\sHEAD[15A⸝asdf/test,founder/17;badges=predictions/pink-2,vip/1,founder/0,bits/1;client-nonce=9b836e232170a9df213aefdcb458b67e;color=#696969;display-name=NotKarar;emotes=;first-msg=0;flags=;id=e00881bd-5f21-4993-8bbd-1736cd13d42e;mod=0;room-id=99631238;subscriber=1;tmi-sent-ts=1653494879409;turbo=0;user-id=89954186;user-type= :notkarar!notkarar@notkarar.tmi.twitch.tv PRIVMSG #zneix :-tags)",
{
{"predictions", R"(<<<<<<\sHEAD[15A⸝asdf/test)"},
{"founder", "17"},
},
{
Badge{"predictions", "pink-2"},
Badge{"vip", "1"},
Badge{"founder", "0"},
Badge{"bits", "1"},
},
},
{
R"(@badge-info=predictions/foo/bar/baz;badges=predictions/blue-1,moderator/1,glhf-pledge/1;client-nonce=f73f16228e6e32f8e92b47ab8283b7e1;color=#1E90FF;display-name=zneixbot;emotes=30259:6-12;first-msg=0;flags=;id=9682a5f1-a0b0-45e2-be9f-8074b58c5f8f;mod=1;room-id=99631238;subscriber=0;tmi-sent-ts=1653573594035;turbo=0;user-id=463521670;user-type=mod :zneixbot!zneixbot@zneixbot.tmi.twitch.tv PRIVMSG #zneix :-tags HeyGuys)",
{
{"predictions", "foo/bar/baz"},
},
{
Badge{"predictions", "blue-1"},
Badge{"moderator", "1"},
Badge{"glhf-pledge", "1"},
},
},
{
R"(@badge-info=subscriber/22;badges=broadcaster/1,subscriber/18,glhf-pledge/1;color=#F97304;display-name=zneix;emotes=;first-msg=0;flags=;id=1d99f67f-a566-4416-a4e2-e85d7fce9223;mod=0;room-id=99631238;subscriber=1;tmi-sent-ts=1653612232758;turbo=0;user-id=99631238;user-type= :zneix!zneix@zneix.tmi.twitch.tv PRIVMSG #zneix :-tags)",
{
{"subscriber", "22"},
},
{
Badge{"broadcaster", "1"},
Badge{"subscriber", "18"},
Badge{"glhf-pledge", "1"},
},
},
};
for (const auto &test : testCases)
{
auto privmsg =
Communi::IrcPrivateMessage::fromData(test.input, nullptr);
auto outputBadgeInfo =
TwitchMessageBuilder::parseBadgeInfoTag(privmsg->tags());
EXPECT_EQ(outputBadgeInfo, test.expectedBadgeInfo)
<< "Input for badgeInfo " << test.input.toStdString() << " failed";
auto outputBadges =
SharedMessageBuilder::parseBadgeTag(privmsg->tags());
EXPECT_EQ(outputBadges, test.expectedBadges)
<< "Input for badges " << test.input.toStdString() << " failed";
}
}