From 8020bf2b178e2bdef22ac2a3c86cbed5b853979c Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Mon, 16 Sep 2024 18:26:31 +0200 Subject: [PATCH 1/5] refactor: irc message builder --- benchmarks/CMakeLists.txt | 1 - benchmarks/src/Highlights.cpp | 102 -- src/messages/Message.hpp | 1 + src/messages/MessageBuilder.cpp | 1042 ++++++++--------- src/messages/MessageBuilder.hpp | 204 ++-- src/providers/twitch/IrcMessageHandler.cpp | 88 +- .../IrcMessageHandler/bad-emotes.json | 2 +- .../IrcMessageHandler/bad-emotes2.json | 2 +- tests/snapshots/IrcMessageHandler/emote.json | 2 +- tests/snapshots/IrcMessageHandler/emotes.json | 2 +- tests/src/Filters.cpp | 4 +- 11 files changed, 655 insertions(+), 795 deletions(-) delete mode 100644 benchmarks/src/Highlights.cpp diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index a0e73332e..2677365ed 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -5,7 +5,6 @@ set(benchmark_SOURCES resources/bench.qrc src/Emojis.cpp - src/Highlights.cpp src/FormatTime.cpp src/Helpers.cpp src/LimitedQueue.cpp diff --git a/benchmarks/src/Highlights.cpp b/benchmarks/src/Highlights.cpp deleted file mode 100644 index 69c69db49..000000000 --- a/benchmarks/src/Highlights.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "Application.hpp" -#include "common/Channel.hpp" -#include "controllers/accounts/AccountController.hpp" -#include "controllers/highlights/HighlightController.hpp" -#include "controllers/highlights/HighlightPhrase.hpp" -#include "messages/Message.hpp" -#include "messages/MessageBuilder.hpp" -#include "mocks/BaseApplication.hpp" -#include "mocks/UserData.hpp" -#include "util/Helpers.hpp" - -#include -#include -#include -#include - -using namespace chatterino; - -class BenchmarkMessageBuilder : public MessageBuilder -{ -public: - explicit BenchmarkMessageBuilder( - Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args) - : MessageBuilder(_channel, _ircMessage, _args) - { - } - - virtual MessagePtr build() - { - // PARSE - this->parse(); - this->usernameColor_ = getRandomColor(this->ircMessage->nick()); - - // words - // this->addWords(this->originalMessage_.split(' ')); - - this->message().messageText = this->originalMessage_; - this->message().searchText = this->message().localizedName + " " + - this->userName + ": " + - this->originalMessage_; - return nullptr; - } - - void bench() - { - this->parseHighlights(); - } -}; - -class MockApplication : public mock::BaseApplication -{ -public: - MockApplication() - : highlights(this->settings, &this->accounts) - { - } - - AccountController *getAccounts() override - { - return &this->accounts; - } - HighlightController *getHighlights() override - { - return &this->highlights; - } - - IUserDataController *getUserData() override - { - return &this->userData; - } - - AccountController accounts; - HighlightController highlights; - mock::UserDataController userData; -}; - -static void BM_HighlightTest(benchmark::State &state) -{ - MockApplication mockApplication; - - std::string message = - R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=41:6-13,15-22;flags=;id=a3196c7e-be4c-4b49-9c5a-8b8302b50c2a;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922213730;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm,Kreygasm (no space))"; - auto ircMessage = Communi::IrcMessage::fromData(message.c_str(), nullptr); - auto privMsg = dynamic_cast(ircMessage); - assert(privMsg != nullptr); - MessageParseArgs args; - auto emptyChannel = Channel::getEmpty(); - - for (auto _ : state) - { - state.PauseTiming(); - BenchmarkMessageBuilder b(emptyChannel.get(), privMsg, args); - - b.build(); - state.ResumeTiming(); - - b.bench(); - } -} - -BENCHMARK(BM_HighlightTest); diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index dd0fa26ff..19ccd60fe 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -22,6 +22,7 @@ class ScrollbarHighlight; struct Message; using MessagePtr = std::shared_ptr; +using MessagePtrMut = std::shared_ptr; struct Message { Message(); ~Message(); diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index fca5753a8..b53b1517a 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -42,6 +42,7 @@ #include "util/Helpers.hpp" #include "util/IrcHelpers.hpp" #include "util/QStringHash.hpp" +#include "util/Variant.hpp" #include "widgets/Window.hpp" #include @@ -52,6 +53,7 @@ #include #include +#include #include #include @@ -148,8 +150,7 @@ QUrl getFallbackHighlightSound() } void actuallyTriggerHighlights(const QString &channelName, bool playSound, - const std::optional &customSoundUrl, - bool windowAlert) + const QUrl &customSoundUrl, bool windowAlert) { if (getApp()->getStreamerMode()->isEnabled() && getSettings()->streamerModeMuteMentions) @@ -170,13 +171,8 @@ void actuallyTriggerHighlights(const QString &channelName, bool playSound, if (playSound && resolveFocus) { - // TODO(C++23): optional or_else - QUrl soundUrl; - if (customSoundUrl) - { - soundUrl = *customSoundUrl; - } - else + QUrl soundUrl = customSoundUrl; + if (soundUrl.isEmpty()) { soundUrl = getFallbackHighlightSound(); } @@ -400,36 +396,6 @@ MessagePtr makeSystemMessage(const QString &text, const QTime &time) MessageBuilder::MessageBuilder() : message_(std::make_shared()) - , ircMessage(nullptr) -{ -} - -MessageBuilder::MessageBuilder(Channel *_channel, - const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args) - : twitchChannel(dynamic_cast(_channel)) - , message_(std::make_shared()) - , channel(_channel) - , ircMessage(_ircMessage) - , args(_args) - , tags(this->ircMessage->tags()) - , originalMessage_(_ircMessage->content()) - , action_(_ircMessage->isAction()) -{ -} - -MessageBuilder::MessageBuilder(Channel *_channel, - const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, QString content, - bool isAction) - : twitchChannel(dynamic_cast(_channel)) - , message_(std::make_shared()) - , channel(_channel) - , ircMessage(_ircMessage) - , args(_args) - , tags(this->ircMessage->tags()) - , originalMessage_(content) - , action_(isAction) { } @@ -1013,14 +979,14 @@ Message &MessageBuilder::message() return *this->message_; } -MessagePtr MessageBuilder::release() +MessagePtrMut MessageBuilder::release() { std::shared_ptr ptr; this->message_.swap(ptr); return ptr; } -std::weak_ptr MessageBuilder::weakOf() +std::weak_ptr MessageBuilder::weakOf() { return this->message_; } @@ -1072,221 +1038,36 @@ void MessageBuilder::addLink(const linkparser::Parsed &parsedLink, getApp()->getLinkResolver()->resolve(el->linkInfo()); } -bool MessageBuilder::isIgnored() const +bool MessageBuilder::isIgnored(const QString &originalMessage, + const QString &userID, const Channel *channel) { return isIgnoredMessage({ - /*.message = */ this->originalMessage_, - /*.twitchUserID = */ this->tags.value("user-id").toString(), - /*.isMod = */ this->channel->isMod(), - /*.isBroadcaster = */ this->channel->isBroadcaster(), + .message = originalMessage, + .twitchUserID = userID, + .isMod = channel->isMod(), + .isBroadcaster = channel->isBroadcaster(), }); } -bool MessageBuilder::isIgnoredReply() const +void MessageBuilder::triggerHighlights(const Channel *channel, + const HighlightAlert &alert) { - return isIgnoredMessage({ - /*.message = */ this->originalMessage_, - /*.twitchUserID = */ - this->tags.value("reply-parent-user-id").toString(), - /*.isMod = */ this->channel->isMod(), - /*.isBroadcaster = */ this->channel->isBroadcaster(), - }); -} - -void MessageBuilder::triggerHighlights() -{ - if (this->historicalMessage_) + if (!alert.windowAlert && !alert.playSound) { - // Do nothing. Highlights should not be triggered on historical messages. return; } - - actuallyTriggerHighlights(this->channel->getName(), this->highlightSound_, - this->highlightSoundCustomUrl_, - this->highlightAlert_); -} - -MessagePtr MessageBuilder::build() -{ - assert(this->ircMessage != nullptr); - assert(this->channel != nullptr); - - // PARSE - this->userId_ = this->ircMessage->tag("user-id").toString(); - - this->parse(); - - if (this->userName == this->channel->getName()) - { - this->senderIsBroadcaster = true; - } - - this->message().channelName = this->channel->getName(); - - this->parseMessageID(); - - this->parseRoomID(); - - // If it is a reward it has to be appended first - if (this->args.channelPointRewardId != "") - { - assert(this->twitchChannel != nullptr); - const auto &reward = this->twitchChannel->channelPointReward( - this->args.channelPointRewardId); - if (reward) - { - this->appendChannelPointRewardMessage( - *reward, this->channel->isMod(), - this->channel->isBroadcaster()); - } - } - - this->appendChannelName(); - - if (this->tags.contains("rm-deleted")) - { - this->message().flags.set(MessageFlag::Disabled); - } - - this->historicalMessage_ = this->tags.contains("historical"); - - if (this->tags.contains("msg-id") && - this->tags["msg-id"].toString().split(';').contains( - "highlighted-message")) - { - this->message().flags.set(MessageFlag::RedeemedHighlight); - } - - if (this->tags.contains("first-msg") && - this->tags["first-msg"].toString() == "1") - { - this->message().flags.set(MessageFlag::FirstMessage); - } - - if (this->tags.contains("pinned-chat-paid-amount")) - { - this->message().flags.set(MessageFlag::ElevatedMessage); - } - - if (this->tags.contains("bits")) - { - this->message().flags.set(MessageFlag::CheerMessage); - } - - // reply threads - this->parseThread(); - - // timestamp - this->message().serverReceivedTime = calculateMessageTime(this->ircMessage); - this->emplace(this->message().serverReceivedTime.time()); - - if (this->shouldAddModerationElements()) - { - this->emplace(); - } - - this->appendTwitchBadges(); - - this->appendChatterinoBadges(); - this->appendFfzBadges(); - this->appendSeventvBadges(); - - this->appendUsername(); - - // QString bits; - auto iterator = this->tags.find("bits"); - if (iterator != this->tags.end()) - { - this->hasBits_ = true; - this->bitsLeft = iterator.value().toInt(); - this->bits = iterator.value().toString(); - } - - // Twitch emotes - auto twitchEmotes = parseTwitchEmotes(this->tags, this->originalMessage_, - this->messageOffset_); - - // This runs through all ignored phrases and runs its replacements on this->originalMessage_ - processIgnorePhrases(*getSettings()->ignoredMessages.readOnly(), - this->originalMessage_, twitchEmotes); - - std::sort(twitchEmotes.begin(), twitchEmotes.end(), - [](const auto &a, const auto &b) { - return a.start < b.start; - }); - twitchEmotes.erase(std::unique(twitchEmotes.begin(), twitchEmotes.end(), - [](const auto &first, const auto &second) { - return first.start == second.start; - }), - twitchEmotes.end()); - - // words - QStringList splits = this->originalMessage_.split(' '); - - this->addWords(splits, twitchEmotes); - - QString stylizedUsername = stylizeUsername(this->userName, this->message()); - - this->message().messageText = this->originalMessage_; - this->message().searchText = - stylizedUsername + " " + this->message().localizedName + " " + - this->userName + ": " + this->originalMessage_ + " " + - this->message().searchText; - - // highlights - this->parseHighlights(); - - // highlighting incoming whispers if requested per setting - if (this->args.isReceivedWhisper && getSettings()->highlightInlineWhispers) - { - this->message().flags.set(MessageFlag::HighlightedWhisper, true); - this->message().highlightColor = - ColorProvider::instance().color(ColorType::Whisper); - } - - if (this->thread_) - { - auto &img = getResources().buttons.replyThreadDark; - this->emplace( - Image::fromResourcePixmap(img, 0.15), 2, Qt::gray, - MessageElementFlag::ReplyButton) - ->setLink({Link::ViewThread, this->thread_->rootId()}); - } - else - { - auto &img = getResources().buttons.replyDark; - this->emplace( - Image::fromResourcePixmap(img, 0.15), 2, Qt::gray, - MessageElementFlag::ReplyButton) - ->setLink({Link::ReplyToMessage, this->message().id}); - } - - return this->release(); -} - -void MessageBuilder::setThread(std::shared_ptr thread) -{ - this->thread_ = std::move(thread); -} - -void MessageBuilder::setParent(MessagePtr parent) -{ - this->parent_ = std::move(parent); -} - -void MessageBuilder::setMessageOffset(int offset) -{ - this->messageOffset_ = offset; + actuallyTriggerHighlights(channel->getName(), alert.playSound, + alert.customSound, alert.windowAlert); } void MessageBuilder::appendChannelPointRewardMessage( const ChannelPointReward &reward, bool isMod, bool isBroadcaster) { if (isIgnoredMessage({ - /*.message = */ "", - /*.twitchUserID = */ reward.user.id, - /*.isMod = */ isMod, - /*.isBroadcaster = */ isBroadcaster, + .message = {}, + .twitchUserID = reward.user.id, + .isMod = isMod, + .isBroadcaster = isBroadcaster, })) { return; @@ -1339,7 +1120,10 @@ void MessageBuilder::appendChannelPointRewardMessage( textList.append({redeemed, reward.title, QString::number(reward.cost)}); this->message().messageText = textList.join(" "); this->message().searchText = textList.join(" "); - this->message().loginName = reward.user.login; + if (!reward.user.login.isEmpty()) + { + this->message().loginName = reward.user.login; + } this->message().reward = std::make_shared(reward); } @@ -1763,9 +1547,10 @@ std::pair MessageBuilder::makeAutomodMessage( {}, {}, action.target.login, action.message, message2->flags); if (highlighted) { - actuallyTriggerHighlights(channelName, highlightResult.playSound, - highlightResult.customSoundUrl, - highlightResult.alert); + actuallyTriggerHighlights( + channelName, highlightResult.playSound, + highlightResult.customSoundUrl.value_or(QUrl{}), + highlightResult.alert); } return std::make_pair(message1, message2); @@ -2022,16 +1807,217 @@ MessagePtr MessageBuilder::makeLowTrustUpdateMessage( return builder.release(); } -void MessageBuilder::addTextOrEmoji(EmotePtr emote) +std::pair MessageBuilder::makeIrcMessage( + /* mutable */ Channel *channel, const Communi::IrcMessage *ircMessage, + const MessageParseArgs &args, /* mutable */ QString content, + const QString::size_type messageOffset, + const std::shared_ptr &thread, const MessagePtr &parent) +{ + assert(ircMessage != nullptr); + assert(channel != nullptr); + + auto tags = ircMessage->tags(); + if (args.allowIgnore) + { + bool ignored = MessageBuilder::isIgnored( + content, tags.value("user-id").toString(), channel); + if (ignored) + { + return {}; + } + } + + auto *twitchChannel = dynamic_cast(channel); + + auto userID = tags.value("user-id").toString(); + + MessageBuilder builder; + builder.parseUsernameColor(tags, userID); + + if (args.isAction) + { + builder.textColor_ = builder.message_->usernameColor; + builder->flags.set(MessageFlag::Action); + } + + builder.parseUsername(ircMessage, twitchChannel, + args.trimSubscriberUsername); + + builder->flags.set(MessageFlag::Collapsed); + + bool senderIsBroadcaster = builder->loginName == channel->getName(); + + builder->channelName = channel->getName(); + + builder.parseMessageID(tags); + + MessageBuilder::parseRoomID(tags, twitchChannel); + twitchChannel = builder.parseSharedChatInfo(tags, twitchChannel); + + // If it is a reward it has to be appended first + if (!args.channelPointRewardId.isEmpty()) + { + assert(twitchChannel != nullptr); + auto reward = + twitchChannel->channelPointReward(args.channelPointRewardId); + if (reward) + { + builder.appendChannelPointRewardMessage(*reward, channel->isMod(), + channel->isBroadcaster()); + } + } + + builder.appendChannelName(channel); + + if (tags.contains("rm-deleted")) + { + builder->flags.set(MessageFlag::Disabled); + } + + if (tags.contains("msg-id") && + tags["msg-id"].toString().split(';').contains("highlighted-message")) + { + builder->flags.set(MessageFlag::RedeemedHighlight); + } + + if (tags.contains("first-msg") && tags["first-msg"].toString() == "1") + { + builder->flags.set(MessageFlag::FirstMessage); + } + + if (tags.contains("pinned-chat-paid-amount")) + { + builder->flags.set(MessageFlag::ElevatedMessage); + } + + if (tags.contains("bits")) + { + builder->flags.set(MessageFlag::CheerMessage); + } + + // reply threads + builder.parseThread(content, tags, channel, thread, parent); + + // timestamp + builder->serverReceivedTime = calculateMessageTime(ircMessage); + builder.emplace(builder->serverReceivedTime.time()); + + bool shouldAddModerationElements = [&] { + if (senderIsBroadcaster) + { + // You cannot timeout the broadcaster + return false; + } + + if (tags.value("user-type").toString() == "mod" && + !args.isStaffOrBroadcaster) + { + // You cannot timeout moderators UNLESS you are Twitch Staff or the broadcaster of the channel + return false; + } + + return true; + }(); + if (shouldAddModerationElements) + { + builder.emplace(); + } + + builder.appendTwitchBadges(tags, twitchChannel); + + builder.appendChatterinoBadges(userID); + builder.appendFfzBadges(twitchChannel, userID); + builder.appendSeventvBadges(userID); + + builder.appendUsername(tags, args); + + TextState textState{.twitchChannel = twitchChannel}; + QString bits; + + auto iterator = tags.find("bits"); + if (iterator != tags.end()) + { + textState.hasBits = true; + textState.bitsLeft = iterator.value().toInt(); + bits = iterator.value().toString(); + } + + // Twitch emotes + auto twitchEmotes = + parseTwitchEmotes(tags, content, static_cast(messageOffset)); + + // This runs through all ignored phrases and runs its replacements on content + processIgnorePhrases(*getSettings()->ignoredMessages.readOnly(), content, + twitchEmotes); + + std::ranges::sort(twitchEmotes, [](const auto &a, const auto &b) { + return a.start < b.start; + }); + auto uniqueEmotes = std::ranges::unique( + twitchEmotes, [](const auto &first, const auto &second) { + return first.start == second.start; + }); + twitchEmotes.erase(uniqueEmotes.begin(), uniqueEmotes.end()); + + // words + QStringList splits = content.split(' '); + + builder.addWords(splits, twitchEmotes, textState); + + QString stylizedUsername = + stylizeUsername(builder->loginName, builder.message()); + + builder->messageText = content; + builder->searchText = stylizedUsername + " " + builder->localizedName + + " " + builder->loginName + ": " + content + " " + + builder->searchText; + + // highlights + HighlightAlert highlight = builder.parseHighlights(tags, content, args); + if (tags.contains("historical")) + { + highlight.playSound = false; + highlight.windowAlert = false; + } + + // highlighting incoming whispers if requested per setting + if (args.isReceivedWhisper && getSettings()->highlightInlineWhispers) + { + builder->flags.set(MessageFlag::HighlightedWhisper); + builder->highlightColor = + ColorProvider::instance().color(ColorType::Whisper); + } + + if (thread) + { + auto &img = getResources().buttons.replyThreadDark; + builder + .emplace(Image::fromResourcePixmap(img, 0.15), + 2, Qt::gray, + MessageElementFlag::ReplyButton) + ->setLink({Link::ViewThread, thread->rootId()}); + } + else + { + auto &img = getResources().buttons.replyDark; + builder + .emplace(Image::fromResourcePixmap(img, 0.15), + 2, Qt::gray, + MessageElementFlag::ReplyButton) + ->setLink({Link::ReplyToMessage, builder->id}); + } + + return {builder.release(), highlight}; +} + +void MessageBuilder::addEmoji(EmotePtr emote) { this->emplace(emote, MessageElementFlag::EmojiAll); } -void MessageBuilder::addTextOrEmoji(const QString &string_) +void MessageBuilder::addTextOrEmote(TextState &state, QString string) { - auto string = QString(string_); - - if (this->hasBits_ && this->tryParseCheermote(string)) + if (state.hasBits && this->tryAppendCheermote(state, string)) { // This string was parsed as a cheermote return; @@ -2042,7 +2028,7 @@ void MessageBuilder::addTextOrEmoji(const QString &string_) // Emote name: "forsenPuke" - if string in ignoredEmotes // Will match emote regardless of source (i.e. bttv, ffz) // Emote source + name: "bttv:nyanPls" - if (this->tryAppendEmote({string})) + if (this->tryAppendEmote(state.twitchChannel, {string})) { // Successfully appended an emote return; @@ -2067,10 +2053,10 @@ void MessageBuilder::addTextOrEmoji(const QString &string_) QString username = match.captured(1); auto originalTextColor = textColor; - if (this->twitchChannel != nullptr) + if (state.twitchChannel != nullptr) { if (auto userColor = - this->twitchChannel->getUserColor(username); + state.twitchChannel->getUserColor(username); userColor.isValid()) { textColor = userColor; @@ -2093,17 +2079,17 @@ void MessageBuilder::addTextOrEmoji(const QString &string_) } } - if (this->twitchChannel != nullptr && getSettings()->findAllUsernames) + if (state.twitchChannel != nullptr && getSettings()->findAllUsernames) { auto match = allUsernamesMentionRegex.match(string); QString username = match.captured(1); if (match.hasMatch() && - this->twitchChannel->accessChatters()->contains(username)) + state.twitchChannel->accessChatters()->contains(username)) { auto originalTextColor = textColor; - if (auto userColor = this->twitchChannel->getUserColor(username); + if (auto userColor = state.twitchChannel->getUserColor(username); userColor.isValid()) { textColor = userColor; @@ -2155,37 +2141,24 @@ TextElement *MessageBuilder::emplaceSystemTextAndUpdate(const QString &text, MessageColor::System); } -void MessageBuilder::parse() -{ - this->parseUsernameColor(); - - if (this->action_) - { - this->textColor_ = this->usernameColor_; - this->message().flags.set(MessageFlag::Action); - } - - this->parseUsername(); - - this->message().flags.set(MessageFlag::Collapsed); -} - -void MessageBuilder::parseUsernameColor() +void MessageBuilder::parseUsernameColor(const QVariantMap &tags, + const QString &userID) { const auto *userData = getApp()->getUserData(); assert(userData != nullptr); - if (const auto &user = userData->getUser(this->userId_)) + if (const auto &user = userData->getUser(userID)) { if (user->color) { this->usernameColor_ = user->color.value(); + this->message().usernameColor = this->usernameColor_; return; } } - const auto iterator = this->tags.find("color"); - if (iterator != this->tags.end()) + const auto iterator = tags.find("color"); + if (iterator != tags.end()) { if (const auto color = iterator.value().toString(); !color.isEmpty()) { @@ -2195,117 +2168,140 @@ void MessageBuilder::parseUsernameColor() } } - if (getSettings()->colorizeNicknames && this->tags.contains("user-id")) + if (getSettings()->colorizeNicknames && tags.contains("user-id")) { - this->usernameColor_ = - getRandomColor(this->tags.value("user-id").toString()); + this->usernameColor_ = getRandomColor(tags.value("user-id").toString()); this->message().usernameColor = this->usernameColor_; } } -void MessageBuilder::parseUsername() +void MessageBuilder::parseUsername(const Communi::IrcMessage *ircMessage, + TwitchChannel *twitchChannel, + bool trimSubscriberUsername) { // username - this->userName = this->ircMessage->nick(); + auto userName = ircMessage->nick(); - this->message().loginName = this->userName; - - if (this->userName.isEmpty() || this->args.trimSubscriberUsername) + if (userName.isEmpty() || trimSubscriberUsername) { - this->userName = this->tags.value(QLatin1String("login")).toString(); + userName = ircMessage->tag("login").toString(); } - // display name - // auto displayNameVariant = this->tags.value("display-name"); - // if (displayNameVariant.isValid()) { - // this->userName = displayNameVariant.toString() + " (" + - // this->userName + ")"; - // } - - this->message().loginName = this->userName; - if (this->twitchChannel != nullptr) + this->message_->loginName = userName; + if (twitchChannel != nullptr) { - this->twitchChannel->setUserColor(this->userName, this->usernameColor_); + twitchChannel->setUserColor(userName, this->message_->usernameColor); } // Update current user color if this is our message auto currentUser = getApp()->getAccounts()->twitch.getCurrent(); - if (this->ircMessage->nick() == currentUser->getUserName()) + if (ircMessage->nick() == currentUser->getUserName()) { - currentUser->setColor(this->usernameColor_); + currentUser->setColor(this->message_->usernameColor); } } -void MessageBuilder::parseMessageID() +void MessageBuilder::parseMessageID(const QVariantMap &tags) { - auto iterator = this->tags.find("id"); + auto iterator = tags.find("id"); - if (iterator != this->tags.end()) + if (iterator != tags.end()) { this->message().id = iterator.value().toString(); } } -void MessageBuilder::parseRoomID() +QString MessageBuilder::parseRoomID(const QVariantMap &tags, + TwitchChannel *twitchChannel) { - if (this->twitchChannel == nullptr) + if (twitchChannel == nullptr) { - return; + return {}; } - auto iterator = this->tags.find("room-id"); + auto iterator = tags.find("room-id"); - if (iterator != std::end(this->tags)) + if (iterator != std::end(tags)) { - this->roomID_ = iterator.value().toString(); - - if (this->twitchChannel->roomId().isEmpty()) + auto roomID = iterator->toString(); + if (twitchChannel->roomId() != roomID) { - this->twitchChannel->setRoomId(this->roomID_); - } - - if (auto it = this->tags.find("source-room-id"); it != this->tags.end()) - { - auto sourceRoom = it.value().toString(); - if (this->roomID_ != sourceRoom) + if (twitchChannel->roomId().isEmpty()) { - this->message().flags.set(MessageFlag::SharedMessage); + twitchChannel->setRoomId(roomID); + } + else + { + qCWarning(chatterinoTwitch) + << "The room-ID of the received message doesn't match the " + "room-ID of the channel - received:" + << roomID << "chanel:" << twitchChannel->roomId(); + } + } + return roomID; + } - auto sourceChan = - getApp()->getTwitch()->getChannelOrEmptyByID(sourceRoom); - if (sourceChan && !sourceChan->isEmpty()) + return {}; +} + +TwitchChannel *MessageBuilder::parseSharedChatInfo(const QVariantMap &tags, + TwitchChannel *twitchChannel) +{ + if (!twitchChannel) + { + return twitchChannel; + } + + if (auto it = tags.find("source-room-id"); it != tags.end()) + { + auto sourceRoom = it.value().toString(); + if (twitchChannel->roomId() != sourceRoom) + { + this->message().flags.set(MessageFlag::SharedMessage); + + auto sourceChan = + getApp()->getTwitch()->getChannelOrEmptyByID(sourceRoom); + if (sourceChan && !sourceChan->isEmpty()) + { + // avoid duplicate pings + this->message().flags.set( + MessageFlag::DoNotTriggerNotification); + + auto *chan = dynamic_cast(sourceChan.get()); + if (chan) { - this->sourceChannel = - dynamic_cast(sourceChan.get()); - // avoid duplicate pings - this->message().flags.set( - MessageFlag::DoNotTriggerNotification); + return chan; } } } } + return twitchChannel; } -void MessageBuilder::parseThread() +void MessageBuilder::parseThread(const QString &messageContent, + const QVariantMap &tags, + const Channel *channel, + const std::shared_ptr &thread, + const MessagePtr &parent) { - if (this->thread_) + if (thread) { // set references - this->message().replyThread = this->thread_; - this->message().replyParent = this->parent_; - this->thread_->addToThread(this->weakOf()); + this->message().replyThread = thread; + this->message().replyParent = parent; + thread->addToThread(std::weak_ptr{this->message_}); // enable reply flag this->message().flags.set(MessageFlag::ReplyMessage); MessagePtr threadRoot; - if (!this->parent_) + if (!parent) { - threadRoot = this->thread_->root(); + threadRoot = thread->root(); } else { - threadRoot = this->parent_; + threadRoot = parent; } QString usernameText = @@ -2317,7 +2313,7 @@ void MessageBuilder::parseThread() this->emplace( "Replying to", MessageElementFlag::RepliedMessage, MessageColor::System, FontStyle::ChatMediumSmall) - ->setLink({Link::ViewThread, this->thread_->rootId()}); + ->setLink({Link::ViewThread, thread->rootId()}); this->emplace( "@" + usernameText + @@ -2336,18 +2332,17 @@ void MessageBuilder::parseThread() MessageElementFlags({MessageElementFlag::RepliedMessage, MessageElementFlag::Text}), color, FontStyle::ChatMediumSmall) - ->setLink({Link::ViewThread, this->thread_->rootId()}); + ->setLink({Link::ViewThread, thread->rootId()}); } - else if (this->tags.find("reply-parent-msg-id") != this->tags.end()) + else if (tags.find("reply-parent-msg-id") != tags.end()) { // Message is a reply but we couldn't find the original message. // Render the message using the additional reply tags - auto replyDisplayName = this->tags.find("reply-parent-display-name"); - auto replyBody = this->tags.find("reply-parent-msg-body"); + auto replyDisplayName = tags.find("reply-parent-display-name"); + auto replyBody = tags.find("reply-parent-msg-body"); - if (replyDisplayName != this->tags.end() && - replyBody != this->tags.end()) + if (replyDisplayName != tags.end() && replyBody != tags.end()) { QString body; @@ -2356,7 +2351,10 @@ void MessageBuilder::parseThread() "Replying to", MessageElementFlag::RepliedMessage, MessageColor::System, FontStyle::ChatMediumSmall); - if (this->isIgnoredReply()) + bool ignored = MessageBuilder::isIgnored( + messageContent, tags.value("reply-parent-user-id").toString(), + channel); + if (ignored) { body = QString("[Blocked user]"); } @@ -2380,67 +2378,76 @@ void MessageBuilder::parseThread() } } -void MessageBuilder::parseHighlights() +HighlightAlert MessageBuilder::parseHighlights(const QVariantMap &tags, + const QString &originalMessage, + const MessageParseArgs &args) { if (getSettings()->isBlacklistedUser(this->message().loginName)) { // Do nothing. We ignore highlights from this user. - return; + return {}; } - auto badges = parseBadgeTag(this->tags); + auto badges = parseBadgeTag(tags); auto [highlighted, highlightResult] = getApp()->getHighlights()->check( - this->args, badges, this->message().loginName, this->originalMessage_, + args, badges, this->message().loginName, originalMessage, this->message().flags); if (!highlighted) { - return; + return {}; } // This message triggered one or more highlights, act upon the highlight result this->message().flags.set(MessageFlag::Highlighted); - this->highlightAlert_ = highlightResult.alert; - - this->highlightSound_ = highlightResult.playSound; - this->highlightSoundCustomUrl_ = highlightResult.customSoundUrl; - this->message().highlightColor = highlightResult.color; if (highlightResult.showInMentions) { this->message().flags.set(MessageFlag::ShowInMentions); } + + auto customSound = [&] { + if (highlightResult.customSoundUrl) + { + return *highlightResult.customSoundUrl; + } + return QUrl{}; + }(); + return { + .customSound = customSound, + .playSound = highlightResult.playSound, + .windowAlert = highlightResult.alert, + }; } -void MessageBuilder::appendChannelName() +void MessageBuilder::appendChannelName(const Channel *channel) { - QString channelName("#" + this->channel->getName()); - Link link(Link::JumpToChannel, this->channel->getName()); + QString channelName("#" + channel->getName()); + Link link(Link::JumpToChannel, channel->getName()); this->emplace(channelName, MessageElementFlag::ChannelName, MessageColor::System) ->setLink(link); } -void MessageBuilder::appendUsername() +void MessageBuilder::appendUsername(const QVariantMap &tags, + const MessageParseArgs &args) { auto *app = getApp(); - QString username = this->userName; - this->message().loginName = username; + QString username = this->message_->loginName; QString localizedName; - auto iterator = this->tags.find("display-name"); - if (iterator != this->tags.end()) + auto iterator = tags.find("display-name"); + if (iterator != tags.end()) { QString displayName = parseTagString(iterator.value().toString()).trimmed(); - if (QString::compare(displayName, this->userName, - Qt::CaseInsensitive) == 0) + if (QString::compare(displayName, username, Qt::CaseInsensitive) == 0) { username = displayName; @@ -2457,13 +2464,13 @@ void MessageBuilder::appendUsername() QString usernameText = stylizeUsername(username, this->message()); - if (this->args.isSentWhisper) + if (args.isSentWhisper) { // TODO(pajlada): Re-implement // userDisplayString += // IrcManager::instance().getUser().getUserName(); } - else if (this->args.isReceivedWhisper) + else if (args.isReceivedWhisper) { // Sender username this->emplace(usernameText, MessageElementFlag::Username, @@ -2488,7 +2495,7 @@ void MessageBuilder::appendUsername() } else { - if (!this->action_) + if (!args.isAction) { usernameText += ":"; } @@ -2500,19 +2507,14 @@ void MessageBuilder::appendUsername() } } -const TwitchChannel *MessageBuilder::getSourceChannel() const +Outcome MessageBuilder::tryAppendEmote(TwitchChannel *twitchChannel, + const EmoteName &name) { - if (this->sourceChannel != nullptr) - { - return this->sourceChannel; - } + auto *app = getApp(); - return this->twitchChannel; -} + auto flags = MessageElementFlags(); + auto emote = std::optional{}; -std::tuple, MessageElementFlags, bool> - MessageBuilder::parseEmote(const EmoteName &name) const -{ // Emote order: // - FrankerFaceZ Channel // - BetterTTV Channel @@ -2520,137 +2522,98 @@ std::tuple, MessageElementFlags, bool> // - FrankerFaceZ Global // - BetterTTV Global // - 7TV Global - - const auto *globalFfzEmotes = getApp()->getFfzEmotes(); - const auto *globalBttvEmotes = getApp()->getBttvEmotes(); - const auto *globalSeventvEmotes = getApp()->getSeventvEmotes(); - - const auto *sourceChannel = this->getSourceChannel(); - - std::optional emote{}; - - if (sourceChannel != nullptr) - { - // Check for channel emotes - - emote = sourceChannel->ffzEmote(name); - if (emote) + [&] { + if (twitchChannel) { - return { - emote, - MessageElementFlag::FfzEmote, - false, - }; - } - - emote = sourceChannel->bttvEmote(name); - if (emote) - { - return { - emote, - MessageElementFlag::BttvEmote, - false, - }; - } - - emote = sourceChannel->seventvEmote(name); - if (emote) - { - return { - emote, - MessageElementFlag::SevenTVEmote, - emote.value()->zeroWidth, - }; - } - } - - // Check for global emotes - - emote = globalFfzEmotes->emote(name); - if (emote) - { - return { - emote, - MessageElementFlag::FfzEmote, - false, - }; - } - - emote = globalBttvEmotes->emote(name); - if (emote) - { - return { - emote, - MessageElementFlag::BttvEmote, - zeroWidthEmotes.contains(name.string), - }; - } - - emote = globalSeventvEmotes->globalEmote(name); - if (emote) - { - return { - emote, - MessageElementFlag::SevenTVEmote, - emote.value()->zeroWidth, - }; - } - - return { - {}, - {}, - false, - }; -} - -Outcome MessageBuilder::tryAppendEmote(const EmoteName &name) -{ - const auto [emote, flags, zeroWidth] = this->parseEmote(name); - - if (emote) - { - if (zeroWidth && getSettings()->enableZeroWidthEmotes && - !this->isEmpty()) - { - // Attempt to merge current zero-width emote into any previous emotes - auto *asEmote = dynamic_cast(&this->back()); - if (asEmote) + emote = twitchChannel->ffzEmote(name); + if (emote) { - // Make sure to access asEmote before taking ownership when releasing - auto baseEmote = asEmote->getEmote(); - // Need to remove EmoteElement and replace with LayeredEmoteElement - auto baseEmoteElement = this->releaseBack(); - - std::vector layers = { - {baseEmote, baseEmoteElement->getFlags()}, {*emote, flags}}; - this->emplace( - std::move(layers), baseEmoteElement->getFlags() | flags, - this->textColor_); - return Success; + flags = MessageElementFlag::FfzEmote; + return; } - auto *asLayered = - dynamic_cast(&this->back()); - if (asLayered) + emote = twitchChannel->bttvEmote(name); + if (emote) { - asLayered->addEmoteLayer({*emote, flags}); - asLayered->addFlags(flags); - return Success; + flags = MessageElementFlag::BttvEmote; + return; } - // No emote to merge with, just show as regular emote + emote = twitchChannel->seventvEmote(name); + if (emote) + { + flags = MessageElementFlag::SevenTVEmote; + return; + } } - this->emplace(*emote, flags, this->textColor_); - return Success; + // check for global emotes + emote = app->getFfzEmotes()->emote(name); + if (emote) + { + flags = MessageElementFlag::FfzEmote; + return; + } + + emote = app->getBttvEmotes()->emote(name); + if (emote) + { + flags = MessageElementFlag::BttvEmote; + return; + } + + emote = app->getSeventvEmotes()->globalEmote(name); + if (emote) + { + flags = MessageElementFlag::SevenTVEmote; + return; + } + }(); + + if (!emote) + { + return Failure; } - return Failure; + bool zeroWidth = emote.value()->zeroWidth; + + if (zeroWidth && getSettings()->enableZeroWidthEmotes && !this->isEmpty()) + { + // Attempt to merge current zero-width emote into any previous emotes + auto *asEmote = dynamic_cast(&this->back()); + if (asEmote) + { + // Make sure to access asEmote before taking ownership when releasing + auto baseEmote = asEmote->getEmote(); + // Need to remove EmoteElement and replace with LayeredEmoteElement + auto baseEmoteElement = this->releaseBack(); + + std::vector layers = { + {baseEmote, baseEmoteElement->getFlags()}, {*emote, flags}}; + this->emplace( + std::move(layers), baseEmoteElement->getFlags() | flags, + this->textColor_); + return Success; + } + + auto *asLayered = dynamic_cast(&this->back()); + if (asLayered) + { + asLayered->addEmoteLayer({*emote, flags}); + asLayered->addFlags(flags); + return Success; + } + + // No emote to merge with, just show as regular emote + } + + this->emplace(*emote, flags, this->textColor_); + return Success; } void MessageBuilder::addWords( const QStringList &words, - const std::vector &twitchEmotes) + const std::vector &twitchEmotes, TextState &state) { // cursor currently indicates what character index we're currently operating in the full list of words int cursor = 0; @@ -2700,14 +2663,18 @@ void MessageBuilder::addWords( // 1. Add text before the emote QString preText = word.left(currentTwitchEmote.start - cursor); - for (auto &variant : + for (auto variant : getApp()->getEmotes()->getEmojis()->parse(preText)) { - boost::apply_visitor( - [&](auto &&arg) { - this->addTextOrEmoji(arg); - }, - variant); + boost::apply_visitor(variant::Overloaded{ + [&](EmotePtr emote) { + this->addEmoji(std::move(emote)); + }, + [&](const QString &text) { + this->addTextOrEmote(state, text); + }, + }, + variant); } cursor += preText.size(); @@ -2721,79 +2688,84 @@ void MessageBuilder::addWords( } // split words - for (auto &variant : getApp()->getEmotes()->getEmojis()->parse(word)) + for (auto variant : getApp()->getEmotes()->getEmojis()->parse(word)) { - boost::apply_visitor( - [&](auto &&arg) { - this->addTextOrEmoji(arg); - }, - variant); + boost::apply_visitor(variant::Overloaded{ + [&](EmotePtr emote) { + this->addEmoji(std::move(emote)); + }, + [&](QString text) { + this->addTextOrEmote(state, + std::move(text)); + }, + }, + variant); } cursor += word.size() + 1; } } -void MessageBuilder::appendTwitchBadges() +void MessageBuilder::appendTwitchBadges(const QVariantMap &tags, + TwitchChannel *twitchChannel) { - if (this->twitchChannel == nullptr) + if (twitchChannel == nullptr) { return; } - auto badgeInfos = parseBadgeInfoTag(this->tags); - auto badges = parseBadgeTag(this->tags); - appendBadges(this, badges, badgeInfos, this->twitchChannel); + auto badgeInfos = parseBadgeInfoTag(tags); + auto badges = parseBadgeTag(tags); + appendBadges(this, badges, badgeInfos, twitchChannel); } -void MessageBuilder::appendChatterinoBadges() +void MessageBuilder::appendChatterinoBadges(const QString &userID) { - if (auto badge = getApp()->getChatterinoBadges()->getBadge({this->userId_})) + if (auto badge = getApp()->getChatterinoBadges()->getBadge({userID})) { this->emplace(*badge, MessageElementFlag::BadgeChatterino); } } -void MessageBuilder::appendFfzBadges() +void MessageBuilder::appendFfzBadges(TwitchChannel *twitchChannel, + const QString &userID) { - for (const auto &badge : - getApp()->getFfzBadges()->getUserBadges({this->userId_})) + for (const auto &badge : getApp()->getFfzBadges()->getUserBadges({userID})) { this->emplace( badge.emote, MessageElementFlag::BadgeFfz, badge.color); } - if (this->twitchChannel == nullptr) + if (twitchChannel == nullptr) { return; } - for (const auto &badge : - this->twitchChannel->ffzChannelBadges(this->userId_)) + for (const auto &badge : twitchChannel->ffzChannelBadges(userID)) { this->emplace( badge.emote, MessageElementFlag::BadgeFfz, badge.color); } } -void MessageBuilder::appendSeventvBadges() +void MessageBuilder::appendSeventvBadges(const QString &userID) { - if (auto badge = getApp()->getSeventvBadges()->getBadge({this->userId_})) + if (auto badge = getApp()->getSeventvBadges()->getBadge({userID})) { this->emplace(*badge, MessageElementFlag::BadgeSevenTV); } } -Outcome MessageBuilder::tryParseCheermote(const QString &string) +Outcome MessageBuilder::tryAppendCheermote(TextState &state, + const QString &string) { - if (this->bitsLeft == 0) + if (state.bitsLeft == 0) { return Failure; } - const auto *chan = this->getSourceChannel(); - auto cheerOpt = chan->cheerEmote(string); + auto cheerOpt = state.twitchChannel->cheerEmote(string); if (!cheerOpt) { @@ -2812,7 +2784,7 @@ Outcome MessageBuilder::tryParseCheermote(const QString &string) if (getSettings()->stackBits) { - if (this->bitsStacked) + if (state.bitsStacked) { return Success; } @@ -2830,25 +2802,25 @@ Outcome MessageBuilder::tryParseCheermote(const QString &string) } if (cheerEmote.color != QColor()) { - this->emplace(QString::number(this->bitsLeft), + this->emplace(QString::number(state.bitsLeft), MessageElementFlag::BitsAmount, cheerEmote.color); } - this->bitsStacked = true; + state.bitsStacked = true; return Success; } - if (this->bitsLeft >= cheerValue) + if (state.bitsLeft >= cheerValue) { - this->bitsLeft -= cheerValue; + state.bitsLeft -= cheerValue; } else { QString newString = string; newString.chop(QString::number(cheerValue).length()); - newString += QString::number(cheerValue - this->bitsLeft); + newString += QString::number(cheerValue - state.bitsLeft); - return tryParseCheermote(newString); + return this->tryAppendCheermote(state, newString); } if (cheerEmote.staticEmote) @@ -2873,22 +2845,4 @@ Outcome MessageBuilder::tryParseCheermote(const QString &string) return Success; } -bool MessageBuilder::shouldAddModerationElements() const -{ - if (this->senderIsBroadcaster) - { - // You cannot timeout the broadcaster - return false; - } - - if (this->tags.value("user-type").toString() == "mod" && - !this->args.isStaffOrBroadcaster) - { - // You cannot timeout moderators UNLESS you are Twitch Staff or the broadcaster of the channel - return false; - } - - return true; -} - } // namespace chatterino diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index b353a8bde..9fa6c03cc 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -14,8 +14,6 @@ #include #include -#include -#include #include #include @@ -31,6 +29,7 @@ struct AutomodUserAction; struct AutomodInfoAction; struct Message; using MessagePtr = std::shared_ptr; +using MessagePtrMut = std::shared_ptr; class MessageElement; class TextElement; @@ -68,6 +67,7 @@ struct LiveUpdatesUpdateEmoteSetMessageTag { struct ImageUploaderResultTag { }; +// NOLINTBEGIN(readability-identifier-naming) const SystemMessageTag systemMessage{}; const RaidEntryMessageTag raidEntryMessage{}; const TimeoutMessageTag timeoutMessage{}; @@ -79,6 +79,7 @@ const LiveUpdatesUpdateEmoteSetMessageTag liveUpdatesUpdateEmoteSetMessage{}; // This signifies that you want to construct a message containing the result of // a successful image upload. const ImageUploaderResultTag imageUploaderResultMessage{}; +// NOLINTEND(readability-identifier-naming) MessagePtr makeSystemMessage(const QString &text); MessagePtr makeSystemMessage(const QString &text, const QTime &time); @@ -90,26 +91,22 @@ struct MessageParseArgs { bool trimSubscriberUsername = false; bool isStaffOrBroadcaster = false; bool isSubscriptionMessage = false; + bool allowIgnore = true; + bool isAction = false; QString channelPointRewardId = ""; }; +struct HighlightAlert { + QUrl customSound; + bool playSound = false; + bool windowAlert = false; +}; class MessageBuilder { public: /// Build a message without a base IRC message. MessageBuilder(); - /// Build a message based on an incoming IRC PRIVMSG - explicit MessageBuilder(Channel *_channel, - const Communi::IrcPrivateMessage *_ircMessage, - const MessageParseArgs &_args); - - /// Build a message based on an incoming IRC message (e.g. notice) - explicit MessageBuilder(Channel *_channel, - const Communi::IrcMessage *_ircMessage, - const MessageParseArgs &_args, QString content, - bool isAction); - MessageBuilder(SystemMessageTag, const QString &text, const QTime &time = QTime::currentTime()); MessageBuilder(RaidEntryMessageTag, const QString &text, @@ -157,17 +154,10 @@ public: ~MessageBuilder() = default; - QString userName; - - /// The Twitch Channel the message was received in - TwitchChannel *twitchChannel = nullptr; - /// The Twitch Channel the message was sent in, according to the Shared Chat feature - TwitchChannel *sourceChannel = nullptr; - Message *operator->(); Message &message(); - MessagePtr release(); - std::weak_ptr weakOf(); + MessagePtrMut release(); + std::weak_ptr weakOf(); void append(std::unique_ptr element); void addLink(const linkparser::Parsed &parsedLink, const QString &source); @@ -184,14 +174,8 @@ public: return pointer; } - [[nodiscard]] bool isIgnored() const; - bool isIgnoredReply() const; - void triggerHighlights(); - MessagePtr build(); - - void setThread(std::shared_ptr thread); - void setParent(MessagePtr parent); - void setMessageOffset(int offset); + static void triggerHighlights(const Channel *channel, + const HighlightAlert &alert); void appendChannelPointRewardMessage(const ChannelPointReward &reward, bool isMod, bool isBroadcaster); @@ -231,96 +215,122 @@ public: static MessagePtr makeLowTrustUpdateMessage( const PubSubLowTrustUsersMessage &action); -protected: - void addTextOrEmoji(EmotePtr emote); - void addTextOrEmoji(const QString &string_); + /// @brief Builds a message out of an `ircMessage`. + /// + /// Building a message won't cause highlights to be triggered. They will + /// only be parsed. To trigger highlights (play sound etc.), use + /// triggerHighlights(). + /// + /// @param channel The channel this message was sent to. Must not be + /// `nullptr`. + /// @param ircMessage The original message. This can be any message + /// (PRIVMSG, USERNOTICE, etc.). Its content is not + /// accessed through this parameter but through `content`, + /// as the content might be inside a tag (e.g. gifts in a + /// USERNOTICE). + /// @param args Arguments from parsing a chat message. + /// @param content The message text. This isn't always the entire text. In + /// replies, the leading mention can be cut off. + /// See `messageOffset`. + /// @param messageOffset Starting offset to be used on index-based + /// operations on `content` such as parsing emotes. + /// For example: + /// ircMessage = "@hi there" + /// content = "there" + /// messageOffset_ = 4 + /// The index 6 would resolve to 6 - 4 = 2 => 'e' + /// @param thread The reply thred this message is part of. If there's no + /// thread, this is an empty `shared_ptr`. + /// @param parent The direct parent this message is replying to. This does + /// not need to be the `thread`s root. If this message isn't + /// replying to anything, this is an empty `shared_ptr`. + /// + /// @returns The built messagae and a highlight result. + static std::pair makeIrcMessage( + Channel *channel, const Communi::IrcMessage *ircMessage, + const MessageParseArgs &args, QString content, + QString::size_type messageOffset, + const std::shared_ptr &thread = {}, + const MessagePtr &parent = {}); + +private: + struct TextState { + TwitchChannel *twitchChannel = nullptr; + bool hasBits = false; + bool bitsStacked = false; + int bitsLeft = 0; + }; + void addEmoji(EmotePtr emote); + void addTextOrEmote(TextState &state, QString string); + + Outcome tryAppendCheermote(TextState &state, const QString &string); + Outcome tryAppendEmote(TwitchChannel *twitchChannel, const EmoteName &name); bool isEmpty() const; MessageElement &back(); std::unique_ptr releaseBack(); - MessageColor textColor_ = MessageColor::Text; - // Helper method that emplaces some text stylized as system text // and then appends that text to the QString parameter "toUpdate". // Returns the TextElement that was emplaced. TextElement *emplaceSystemTextAndUpdate(const QString &text, QString &toUpdate); - std::shared_ptr message_; - void parse(); - void parseUsernameColor(); - void parseUsername(); - void parseMessageID(); - void parseRoomID(); + void parseUsernameColor(const QVariantMap &tags, const QString &userID); + void parseUsername(const Communi::IrcMessage *ircMessage, + TwitchChannel *twitchChannel, + bool trimSubscriberUsername); + void parseMessageID(const QVariantMap &tags); + + /// Parses the room-ID this message was received in + /// + /// @returns The room-ID + static QString parseRoomID(const QVariantMap &tags, + TwitchChannel *twitchChannel); + + /// Parses the shared-chat information from this message. + /// + /// @param tags The tags of the received message + /// @param twitchChannel The channel this message was received in + /// @returns The source channel - the channel this message originated from. + /// If there's no channel currently open, @a twitchChannel is + /// returned. + TwitchChannel *parseSharedChatInfo(const QVariantMap &tags, + TwitchChannel *twitchChannel); + // Parse & build thread information into the message // Will read information from thread_ or from IRC tags - void parseThread(); + void parseThread(const QString &messageContent, const QVariantMap &tags, + const Channel *channel, + const std::shared_ptr &thread, + const MessagePtr &parent); // parseHighlights only updates the visual state of the message, but leaves the playing of alerts and sounds to the triggerHighlights function - void parseHighlights(); - void appendChannelName(); - void appendUsername(); + HighlightAlert parseHighlights(const QVariantMap &tags, + const QString &originalMessage, + const MessageParseArgs &args); - /// Return the Twitch Channel this message originated from - /// - /// Useful to handle messages from the "Shared Chat" feature - /// - /// Can return nullptr - const TwitchChannel *getSourceChannel() const; - - std::tuple, MessageElementFlags, bool> parseEmote( - const EmoteName &name) const; - Outcome tryAppendEmote(const EmoteName &name); + void appendChannelName(const Channel *channel); + void appendUsername(const QVariantMap &tags, const MessageParseArgs &args); void addWords(const QStringList &words, - const std::vector &twitchEmotes); + const std::vector &twitchEmotes, + TextState &state); - void appendTwitchBadges(); - void appendChatterinoBadges(); - void appendFfzBadges(); - void appendSeventvBadges(); - Outcome tryParseCheermote(const QString &string); + void appendTwitchBadges(const QVariantMap &tags, + TwitchChannel *twitchChannel); + void appendChatterinoBadges(const QString &userID); + void appendFfzBadges(TwitchChannel *twitchChannel, const QString &userID); + void appendSeventvBadges(const QString &userID); - bool shouldAddModerationElements() const; + [[nodiscard]] static bool isIgnored(const QString &originalMessage, + const QString &userID, + const Channel *channel); - QString roomID_; - bool hasBits_ = false; - QString bits; - int bitsLeft{}; - bool bitsStacked = false; - bool historicalMessage_ = false; - std::shared_ptr thread_; - MessagePtr parent_; - - /** - * Starting offset to be used on index-based operations on `originalMessage_`. - * - * For example: - * originalMessage_ = "there" - * messageOffset_ = 4 - * (the irc message is "hey there") - * - * then the index 6 would resolve to 6 - 4 = 2 => 'e' - */ - int messageOffset_ = 0; - - QString userId_; - bool senderIsBroadcaster{}; - - Channel *channel = nullptr; - const Communi::IrcMessage *ircMessage; - MessageParseArgs args; - const QVariantMap tags; - QString originalMessage_; - - const bool action_{}; + std::shared_ptr message_; + MessageColor textColor_ = MessageColor::Text; QColor usernameColor_ = {153, 153, 153}; - - bool highlightAlert_ = false; - bool highlightSound_ = false; - std::optional highlightSoundCustomUrl_{}; }; } // namespace chatterino diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 7d5bb34a4..9548ea093 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -508,15 +508,17 @@ std::vector parseUserNoticeMessage(Channel *channel, { MessageParseArgs args; args.trimSubscriberUsername = true; + args.allowIgnore = false; - MessageBuilder builder(channel, message, args, content, false); - builder->flags.set(MessageFlag::Subscription); - builder->flags.unset(MessageFlag::Highlighted); + auto [built, highlight] = MessageBuilder::makeIrcMessage( + channel, message, args, content, 0); + built->flags.set(MessageFlag::Subscription); + built->flags.unset(MessageFlag::Highlighted); if (mirrored) { - builder->flags.set(MessageFlag::SharedMessage); + built->flags.set(MessageFlag::SharedMessage); } - builtMessages.emplace_back(builder.build()); + builtMessages.emplace_back(std::move(built)); } } @@ -661,12 +663,12 @@ std::vector parsePrivMessage(Channel *channel, std::vector builtMessages; MessageParseArgs args; - MessageBuilder builder(channel, message, args, message->content(), - message->isAction()); - if (!builder.isIgnored()) + auto [built, alert] = MessageBuilder::makeIrcMessage( + channel, message, args, message->content(), message->isAction(), 0); + if (built) { - builtMessages.emplace_back(builder.build()); - builder.triggerHighlights(); + builtMessages.emplace_back(std::move(built)); + MessageBuilder::triggerHighlights(channel, alert); } return builtMessages; @@ -709,22 +711,21 @@ std::vector IrcMessageHandler::parseMessageWithReply( { args.channelPointRewardId = it.value().toString(); } - MessageBuilder builder(channel, message, args, content, - privMsg->isAction()); - builder.setMessageOffset(messageOffset); + args.isAction = privMsg->isAction(); auto replyCtx = getReplyContext(tc, message, otherLoaded); - builder.setThread(std::move(replyCtx.thread)); - builder.setParent(std::move(replyCtx.parent)); - if (replyCtx.highlight) - { - builder.message().flags.set(MessageFlag::SubscribedThread); - } + auto [built, alert] = MessageBuilder::makeIrcMessage( + channel, message, args, content, messageOffset, replyCtx.thread, + replyCtx.parent); - if (!builder.isIgnored()) + if (built) { - builtMessages.emplace_back(builder.build()); - builder.triggerHighlights(); + if (replyCtx.highlight) + { + built->flags.set(MessageFlag::SubscribedThread); + } + builtMessages.emplace_back(built); + MessageBuilder::triggerHighlights(channel, alert); } if (message->tags().contains(u"pinned-chat-paid-amount"_s)) @@ -1016,20 +1017,18 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage) auto *c = getApp()->getTwitch()->getWhispersChannel().get(); - MessageBuilder builder(c, ircMessage, args, - unescapeZeroWidthJoiner(ircMessage->parameter(1)), - false); - - if (builder.isIgnored()) + auto [message, alert] = MessageBuilder::makeIrcMessage( + c, ircMessage, args, unescapeZeroWidthJoiner(ircMessage->parameter(1)), + false, 0); + if (!message) { return; } - builder->flags.set(MessageFlag::Whisper); - MessagePtr message = builder.build(); - builder.triggerHighlights(); + message->flags.set(MessageFlag::Whisper); + MessageBuilder::triggerHighlights(c, alert); - getApp()->getTwitch()->setLastUserThatWhisperedMe(builder.userName); + getApp()->getTwitch()->setLastUserThatWhisperedMe(message->loginName); if (message->flags.has(MessageFlag::ShowInMentions)) { @@ -1504,6 +1503,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, { args.isStaffOrBroadcaster = true; } + args.isAction = isAction; auto *channel = dynamic_cast(chan.get()); @@ -1605,24 +1605,22 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, } } - MessageBuilder builder(channel, message, args, content, isAction); - builder.setMessageOffset(messageOffset); + args.allowIgnore = !isSub; + auto [msg, alert] = MessageBuilder::makeIrcMessage( + channel, message, args, content, messageOffset, replyCtx.thread, + replyCtx.parent); - builder.setThread(std::move(replyCtx.thread)); - builder.setParent(std::move(replyCtx.parent)); - if (replyCtx.highlight) - { - builder.message().flags.set(MessageFlag::SubscribedThread); - } - - if (isSub || !builder.isIgnored()) + if (msg) { if (isSub) { - builder->flags.set(MessageFlag::Subscription); - builder->flags.unset(MessageFlag::Highlighted); + msg->flags.set(MessageFlag::Subscription); + msg->flags.unset(MessageFlag::Highlighted); + } + if (replyCtx.highlight) + { + msg->flags.set(MessageFlag::SubscribedThread); } - auto msg = builder.build(); IrcMessageHandler::setSimilarityFlags(msg, chan); @@ -1630,7 +1628,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, (!getSettings()->hideSimilar && getSettings()->shownSimilarTriggerHighlights)) { - builder.triggerHighlights(); + MessageBuilder::triggerHighlights(channel, alert); } const auto highlighted = msg->flags.has(MessageFlag::Highlighted); diff --git a/tests/snapshots/IrcMessageHandler/bad-emotes.json b/tests/snapshots/IrcMessageHandler/bad-emotes.json index 33314c1e8..86714b028 100644 --- a/tests/snapshots/IrcMessageHandler/bad-emotes.json +++ b/tests/snapshots/IrcMessageHandler/bad-emotes.json @@ -153,7 +153,7 @@ "searchText": "mm2pl mm2pl: Kappa ", "serverReceivedTime": "2022-09-03T10:31:42Z", "timeoutUser": "", - "usernameColor": "#ff000000" + "usernameColor": "#ffdaa521" } ] } diff --git a/tests/snapshots/IrcMessageHandler/bad-emotes2.json b/tests/snapshots/IrcMessageHandler/bad-emotes2.json index 4455bf818..fa8cb19e5 100644 --- a/tests/snapshots/IrcMessageHandler/bad-emotes2.json +++ b/tests/snapshots/IrcMessageHandler/bad-emotes2.json @@ -153,7 +153,7 @@ "searchText": "mm2pl mm2pl: Kappa ", "serverReceivedTime": "2022-09-03T10:31:42Z", "timeoutUser": "", - "usernameColor": "#ff000000" + "usernameColor": "#ffdaa521" } ] } diff --git a/tests/snapshots/IrcMessageHandler/emote.json b/tests/snapshots/IrcMessageHandler/emote.json index 5a9ec3fd2..cb8965388 100644 --- a/tests/snapshots/IrcMessageHandler/emote.json +++ b/tests/snapshots/IrcMessageHandler/emote.json @@ -153,7 +153,7 @@ "searchText": "mm2pl mm2pl: Keepo ", "serverReceivedTime": "2022-09-03T10:31:35Z", "timeoutUser": "", - "usernameColor": "#ff000000" + "usernameColor": "#ffdaa521" } ] } diff --git a/tests/snapshots/IrcMessageHandler/emotes.json b/tests/snapshots/IrcMessageHandler/emotes.json index 19d6bd7bf..e7e6d4a02 100644 --- a/tests/snapshots/IrcMessageHandler/emotes.json +++ b/tests/snapshots/IrcMessageHandler/emotes.json @@ -221,7 +221,7 @@ "searchText": "mm2pl mm2pl: Kappa Keepo PogChamp ", "serverReceivedTime": "2022-09-03T10:31:42Z", "timeoutUser": "", - "usernameColor": "#ff000000" + "usernameColor": "#ffdaa521" } ] } diff --git a/tests/src/Filters.cpp b/tests/src/Filters.cpp index b0c728059..6ea757545 100644 --- a/tests/src/Filters.cpp +++ b/tests/src/Filters.cpp @@ -285,9 +285,9 @@ TEST_F(FiltersF, TypingContextChecks) QString originalMessage = privmsg->content(); - MessageBuilder builder(&channel, privmsg, MessageParseArgs{}); + auto [msg, alert] = MessageBuilder::makeIrcMessage( + &channel, privmsg, MessageParseArgs{}, originalMessage, false, 0); - auto msg = builder.build(); EXPECT_NE(msg.get(), nullptr); auto contextMap = buildContextMap(msg, &channel); From c2caef1de45b1df62212a7b42972ba1e5308303f Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 19 Oct 2024 15:58:19 +0200 Subject: [PATCH 2/5] chore: add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d38c0fb..7bf199232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,6 +110,7 @@ - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) - Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652) - Dev: Decoupled reply parsing from `MessageBuilder`. (#5660) +- Dev: Refactored IRC message building. (#5663) ## 2.5.1 From da9b747089c228c83e51d397466dfb271e95070d Mon Sep 17 00:00:00 2001 From: Nerixyz Date: Sat, 19 Oct 2024 17:52:40 +0200 Subject: [PATCH 3/5] fix: why did this even compile??? --- src/messages/MessageBuilder.cpp | 15 ++++++++------- src/messages/MessageBuilder.hpp | 2 +- src/providers/twitch/IrcMessageHandler.cpp | 7 ++++--- tests/src/Filters.cpp | 2 +- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index b53b1517a..7720b9342 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -2010,7 +2010,7 @@ std::pair MessageBuilder::makeIrcMessage( return {builder.release(), highlight}; } -void MessageBuilder::addEmoji(EmotePtr emote) +void MessageBuilder::addEmoji(const EmotePtr &emote) { this->emplace(emote, MessageElementFlag::EmojiAll); } @@ -2667,11 +2667,12 @@ void MessageBuilder::addWords( getApp()->getEmotes()->getEmojis()->parse(preText)) { boost::apply_visitor(variant::Overloaded{ - [&](EmotePtr emote) { - this->addEmoji(std::move(emote)); + [&](const EmotePtr &emote) { + this->addEmoji(emote); }, - [&](const QString &text) { - this->addTextOrEmote(state, text); + [&](QString text) { + this->addTextOrEmote( + state, std::move(text)); }, }, variant); @@ -2691,8 +2692,8 @@ void MessageBuilder::addWords( for (auto variant : getApp()->getEmotes()->getEmojis()->parse(word)) { boost::apply_visitor(variant::Overloaded{ - [&](EmotePtr emote) { - this->addEmoji(std::move(emote)); + [&](const EmotePtr &emote) { + this->addEmoji(emote); }, [&](QString text) { this->addTextOrEmote(state, diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 9fa6c03cc..031084a34 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -260,7 +260,7 @@ private: bool bitsStacked = false; int bitsLeft = 0; }; - void addEmoji(EmotePtr emote); + void addEmoji(const EmotePtr &emote); void addTextOrEmote(TextState &state, QString string); Outcome tryAppendCheermote(TextState &state, const QString &string); diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 9548ea093..cd6544cb7 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -663,8 +663,9 @@ std::vector parsePrivMessage(Channel *channel, std::vector builtMessages; MessageParseArgs args; - auto [built, alert] = MessageBuilder::makeIrcMessage( - channel, message, args, message->content(), message->isAction(), 0); + args.isAction = message->isAction(); + auto [built, alert] = MessageBuilder::makeIrcMessage(channel, message, args, + message->content(), 0); if (built) { builtMessages.emplace_back(std::move(built)); @@ -1019,7 +1020,7 @@ void IrcMessageHandler::handleWhisperMessage(Communi::IrcMessage *ircMessage) auto [message, alert] = MessageBuilder::makeIrcMessage( c, ircMessage, args, unescapeZeroWidthJoiner(ircMessage->parameter(1)), - false, 0); + 0); if (!message) { return; diff --git a/tests/src/Filters.cpp b/tests/src/Filters.cpp index 6ea757545..67cd48d23 100644 --- a/tests/src/Filters.cpp +++ b/tests/src/Filters.cpp @@ -286,7 +286,7 @@ TEST_F(FiltersF, TypingContextChecks) QString originalMessage = privmsg->content(); auto [msg, alert] = MessageBuilder::makeIrcMessage( - &channel, privmsg, MessageParseArgs{}, originalMessage, false, 0); + &channel, privmsg, MessageParseArgs{}, originalMessage, 0); EXPECT_NE(msg.get(), nullptr); From d3eacc5d8d6ce921d7d17ed4d51df07f495efe56 Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sun, 20 Oct 2024 11:09:12 +0200 Subject: [PATCH 4/5] nit: fix typos --- src/messages/MessageBuilder.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 031084a34..584ed5237 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -239,13 +239,13 @@ public: /// content = "there" /// messageOffset_ = 4 /// The index 6 would resolve to 6 - 4 = 2 => 'e' - /// @param thread The reply thred this message is part of. If there's no + /// @param thread The reply thread this message is part of. If there's no /// thread, this is an empty `shared_ptr`. /// @param parent The direct parent this message is replying to. This does /// not need to be the `thread`s root. If this message isn't /// replying to anything, this is an empty `shared_ptr`. /// - /// @returns The built messagae and a highlight result. + /// @returns The built message and a highlight result. static std::pair makeIrcMessage( Channel *channel, const Communi::IrcMessage *ircMessage, const MessageParseArgs &args, QString content, From 802edeb8de77203ae8efb32021570c96e8918bbd Mon Sep 17 00:00:00 2001 From: Rasmus Karlsson Date: Sun, 20 Oct 2024 11:09:16 +0200 Subject: [PATCH 5/5] nit: check makeIrcMessage return value --- src/providers/twitch/IrcMessageHandler.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index cd6544cb7..fb9d2fa13 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -512,13 +512,16 @@ std::vector parseUserNoticeMessage(Channel *channel, auto [built, highlight] = MessageBuilder::makeIrcMessage( channel, message, args, content, 0); - built->flags.set(MessageFlag::Subscription); - built->flags.unset(MessageFlag::Highlighted); - if (mirrored) + if (built) { - built->flags.set(MessageFlag::SharedMessage); + built->flags.set(MessageFlag::Subscription); + built->flags.unset(MessageFlag::Highlighted); + if (mirrored) + { + built->flags.set(MessageFlag::SharedMessage); + } + builtMessages.emplace_back(std::move(built)); } - builtMessages.emplace_back(std::move(built)); } }