From e35fabfabe2f430e5614340a0fc6e5009db415b3 Mon Sep 17 00:00:00 2001 From: nerix Date: Sun, 20 Oct 2024 12:40:48 +0200 Subject: [PATCH 1/6] refactor: irc message builder (#5663) --- CHANGELOG.md | 1 + benchmarks/CMakeLists.txt | 1 - benchmarks/src/Highlights.cpp | 102 -- src/messages/Message.hpp | 1 + src/messages/MessageBuilder.cpp | 1090 ++++++++--------- src/messages/MessageBuilder.hpp | 206 ++-- src/providers/twitch/IrcMessageHandler.cpp | 94 +- .../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 +- 12 files changed, 703 insertions(+), 804 deletions(-) delete mode 100644 benchmarks/src/Highlights.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae647165..1a08aac48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,7 @@ - Dev: Move plugins to Sol2. (#5622) - 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 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..db479adf3 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(); } @@ -384,6 +380,97 @@ EmotePtr makeAutoModBadge() Url{"https://dashboard.twitch.tv/settings/moderation/automod"}}); } +std::tuple, MessageElementFlags, bool> parseEmote( + TwitchChannel *twitchChannel, const EmoteName &name) +{ + // Emote order: + // - FrankerFaceZ Channel + // - BetterTTV Channel + // - 7TV Channel + // - FrankerFaceZ Global + // - BetterTTV Global + // - 7TV Global + + const auto *globalFfzEmotes = getApp()->getFfzEmotes(); + const auto *globalBttvEmotes = getApp()->getBttvEmotes(); + const auto *globalSeventvEmotes = getApp()->getSeventvEmotes(); + + std::optional emote{}; + + if (twitchChannel != nullptr) + { + // Check for channel emotes + + emote = twitchChannel->ffzEmote(name); + if (emote) + { + return { + emote, + MessageElementFlag::FfzEmote, + false, + }; + } + + emote = twitchChannel->bttvEmote(name); + if (emote) + { + return { + emote, + MessageElementFlag::BttvEmote, + false, + }; + } + + emote = twitchChannel->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, + }; +} + } // namespace namespace chatterino { @@ -400,36 +487,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 +1070,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 +1129,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 +1211,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 +1638,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 +1898,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(const 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 +2119,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 +2144,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 +2170,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 +2232,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 +2259,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 << "channel:" << 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 +2404,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 +2423,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 +2442,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 +2469,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 +2555,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 +2586,7 @@ void MessageBuilder::appendUsername() } else { - if (!this->action_) + if (!args.isAction) { usernameText += ":"; } @@ -2500,157 +2598,53 @@ void MessageBuilder::appendUsername() } } -const TwitchChannel *MessageBuilder::getSourceChannel() const +Outcome MessageBuilder::tryAppendEmote(TwitchChannel *twitchChannel, + const EmoteName &name) { - if (this->sourceChannel != nullptr) + auto [emote, flags, zeroWidth] = parseEmote(twitchChannel, name); + + if (!emote) { - return this->sourceChannel; + return Failure; } - return this->twitchChannel; -} - -std::tuple, MessageElementFlags, bool> - MessageBuilder::parseEmote(const EmoteName &name) const -{ - // Emote order: - // - FrankerFaceZ Channel - // - BetterTTV Channel - // - 7TV Channel - // - 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) + if (zeroWidth && getSettings()->enableZeroWidthEmotes && !this->isEmpty()) { - // Check for channel emotes - - emote = sourceChannel->ffzEmote(name); - if (emote) + // Attempt to merge current zero-width emote into any previous emotes + auto *asEmote = dynamic_cast(&this->back()); + if (asEmote) { - return { - emote, - MessageElementFlag::FfzEmote, - false, - }; + // 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; } - emote = sourceChannel->bttvEmote(name); - if (emote) + auto *asLayered = dynamic_cast(&this->back()); + if (asLayered) { - return { - emote, - MessageElementFlag::BttvEmote, - false, - }; + asLayered->addEmoteLayer({*emote, flags}); + asLayered->addFlags(flags); + return Success; } - emote = sourceChannel->seventvEmote(name); - if (emote) - { - return { - emote, - MessageElementFlag::SevenTVEmote, - emote.value()->zeroWidth, - }; - } + // No emote to merge with, just show as regular emote } - // 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) - { - // 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; - } - - return Failure; + 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 +2694,19 @@ 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{ + [&](const EmotePtr &emote) { + this->addEmoji(emote); + }, + [&](QString text) { + this->addTextOrEmote( + state, std::move(text)); + }, + }, + variant); } cursor += preText.size(); @@ -2721,79 +2720,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{ + [&](const EmotePtr &emote) { + this->addEmoji(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 +2816,7 @@ Outcome MessageBuilder::tryParseCheermote(const QString &string) if (getSettings()->stackBits) { - if (this->bitsStacked) + if (state.bitsStacked) { return Success; } @@ -2830,25 +2834,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 +2877,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..45b65095d 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,124 @@ 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 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 message and a highlight result. If the message is + /// ignored (e.g. from a blocked user), then the returned pointer + /// will be en empty `shared_ptr`. + 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(const 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..fb9d2fa13 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -508,15 +508,20 @@ 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); - if (mirrored) + auto [built, highlight] = MessageBuilder::makeIrcMessage( + channel, message, args, content, 0); + if (built) { - builder->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(builder.build()); } } @@ -661,12 +666,13 @@ std::vector parsePrivMessage(Channel *channel, std::vector builtMessages; MessageParseArgs args; - MessageBuilder builder(channel, message, args, message->content(), - message->isAction()); - if (!builder.isIgnored()) + args.isAction = message->isAction(); + auto [built, alert] = MessageBuilder::makeIrcMessage(channel, message, args, + message->content(), 0); + if (built) { - builtMessages.emplace_back(builder.build()); - builder.triggerHighlights(); + builtMessages.emplace_back(std::move(built)); + MessageBuilder::triggerHighlights(channel, alert); } return builtMessages; @@ -709,22 +715,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 +1021,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)), + 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 +1507,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, { args.isStaffOrBroadcaster = true; } + args.isAction = isAction; auto *channel = dynamic_cast(chan.get()); @@ -1605,24 +1609,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 +1632,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..67cd48d23 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, 0); - auto msg = builder.build(); EXPECT_NE(msg.get(), nullptr); auto contextMap = buildContextMap(msg, &channel); From 867e3f3ab08ac8c7bd807f2fe8edfb1518ddcee5 Mon Sep 17 00:00:00 2001 From: nerix Date: Mon, 21 Oct 2024 00:57:37 +0200 Subject: [PATCH 2/6] fix: only invalidate buffers for chat windows (#5666) --- CHANGELOG.md | 2 +- src/widgets/BaseWindow.cpp | 11 +++++++---- src/widgets/BaseWindow.hpp | 1 + src/widgets/DraggablePopup.cpp | 6 +++--- src/widgets/OverlayWindow.cpp | 8 ++++++++ src/widgets/Window.cpp | 4 +++- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a08aac48..3555762ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ - Major: Add option to show pronouns in user card. (#5442, #5583) - Major: Release plugins alpha. (#5288) -- Major: Improve high-DPI support on Windows. (#4868, #5391, #5664) +- Major: Improve high-DPI support on Windows. (#4868, #5391, #5664, #5666) - Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746, #5643, #5659) - Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530) - Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625) diff --git a/src/widgets/BaseWindow.cpp b/src/widgets/BaseWindow.cpp index a82a364f2..31f72a3df 100644 --- a/src/widgets/BaseWindow.cpp +++ b/src/widgets/BaseWindow.cpp @@ -877,10 +877,13 @@ bool BaseWindow::nativeEvent(const QByteArray &eventType, void *message, break; case WM_DPICHANGED: { - // wait for Qt to process this message - postToThread([] { - getApp()->getWindows()->invalidateChannelViewBuffers(); - }); + if (this->flags_.has(ClearBuffersOnDpiChange)) + { + // wait for Qt to process this message + postToThread([] { + getApp()->getWindows()->invalidateChannelViewBuffers(); + }); + } } break; diff --git a/src/widgets/BaseWindow.hpp b/src/widgets/BaseWindow.hpp index 2f2168645..1f0455dce 100644 --- a/src/widgets/BaseWindow.hpp +++ b/src/widgets/BaseWindow.hpp @@ -37,6 +37,7 @@ public: Dialog = 1 << 6, DisableLayoutSave = 1 << 7, BoundsCheckOnShow = 1 << 8, + ClearBuffersOnDpiChange = 1 << 9, }; enum ActionOnFocusLoss { Nothing, Delete, Close, Hide }; diff --git a/src/widgets/DraggablePopup.cpp b/src/widgets/DraggablePopup.cpp index 84a57c0f7..6b7e9eaf9 100644 --- a/src/widgets/DraggablePopup.cpp +++ b/src/widgets/DraggablePopup.cpp @@ -36,9 +36,9 @@ namespace { DraggablePopup::DraggablePopup(bool closeAutomatically, QWidget *parent) : BaseWindow( - closeAutomatically - ? popupFlagsCloseAutomatically | BaseWindow::DisableLayoutSave - : popupFlags | BaseWindow::DisableLayoutSave, + (closeAutomatically ? popupFlagsCloseAutomatically : popupFlags) | + BaseWindow::DisableLayoutSave | + BaseWindow::ClearBuffersOnDpiChange, parent) , lifetimeHack_(std::make_shared(false)) , dragTimer_(this) diff --git a/src/widgets/OverlayWindow.cpp b/src/widgets/OverlayWindow.cpp index b6cff796e..ce7ee2489 100644 --- a/src/widgets/OverlayWindow.cpp +++ b/src/widgets/OverlayWindow.cpp @@ -7,6 +7,7 @@ #include "singletons/Emotes.hpp" #include "singletons/Settings.hpp" #include "singletons/WindowManager.hpp" +#include "util/PostToThread.hpp" #include "widgets/BaseWidget.hpp" #include "widgets/helper/ChannelView.hpp" #include "widgets/helper/InvisibleSizeGrip.hpp" @@ -312,6 +313,13 @@ bool OverlayWindow::nativeEvent(const QByteArray &eventType, void *message, } break; # endif + case WM_DPICHANGED: { + // wait for Qt to process this message, same as in BaseWindow + postToThread([] { + getApp()->getWindows()->invalidateChannelViewBuffers(); + }); + } + break; default: return QWidget::nativeEvent(eventType, message, result); diff --git a/src/widgets/Window.cpp b/src/widgets/Window.cpp index 4dcb14503..92b85c725 100644 --- a/src/widgets/Window.cpp +++ b/src/widgets/Window.cpp @@ -52,7 +52,9 @@ namespace chatterino { Window::Window(WindowType type, QWidget *parent) - : BaseWindow(BaseWindow::EnableCustomFrame, parent) + : BaseWindow( + {BaseWindow::EnableCustomFrame, BaseWindow::ClearBuffersOnDpiChange}, + parent) , type_(type) , notebook_(new SplitNotebook(this)) { From 45d2c292d02a498aaad9b4b4181df0b2b1611e89 Mon Sep 17 00:00:00 2001 From: pajlada Date: Mon, 21 Oct 2024 13:19:08 +0200 Subject: [PATCH 3/6] fix: subscribed threads not being marked as subscribed (#5668) --- CHANGELOG.md | 2 +- src/messages/MessageBuilder.cpp | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3555762ce..c95aa2df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,7 +110,7 @@ - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) - Dev: Move plugins to Sol2. (#5622) - Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652) -- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660) +- Dev: Decoupled reply parsing from `MessageBuilder`. (#5660, #5668) - Dev: Refactored IRC message building. (#5663) ## 2.5.1 diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index db479adf3..a17f955a9 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -2382,6 +2382,11 @@ void MessageBuilder::parseThread(const QString &messageContent, this->message().replyParent = parent; thread->addToThread(std::weak_ptr{this->message_}); + if (thread->subscribed()) + { + this->message().flags.set(MessageFlag::SubscribedThread); + } + // enable reply flag this->message().flags.set(MessageFlag::ReplyMessage); From 2ec8fa8723e3e894f02dafc1c3065a2ce2765dd5 Mon Sep 17 00:00:00 2001 From: pajlada Date: Mon, 21 Oct 2024 19:22:23 +0200 Subject: [PATCH 4/6] refactor: remove unused ReplyContext.highlight (#5669) --- src/providers/twitch/IrcMessageHandler.cpp | 66 +++++++--------------- 1 file changed, 20 insertions(+), 46 deletions(-) diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index fb9d2fa13..0c345da83 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -123,47 +123,34 @@ int stripLeadingReplyMention(const QVariantMap &tags, QString &content) return 0; } -[[nodiscard]] bool shouldHighlightReplyThread( - const QVariantMap &tags, const QString &senderLogin, - std::shared_ptr &thread, bool isNew) +void checkThreadSubscription(const QVariantMap &tags, + const QString &senderLogin, + std::shared_ptr &thread) { - const auto ¤tLogin = - getApp()->getAccounts()->twitch.getCurrent()->getUserName(); - - if (thread->subscribed()) + if (thread->subscribed() || thread->unsubscribed()) { - return true; - } - - if (thread->unsubscribed()) - { - return false; + return; } if (getSettings()->autoSubToParticipatedThreads) { - if (isNew) - { - if (const auto it = tags.find("reply-parent-user-login"); - it != tags.end()) - { - auto name = it.value().toString(); - if (name == currentLogin) - { - thread->markSubscribed(); - return true; // already marked as participated - } - } - } + const auto ¤tLogin = + getApp()->getAccounts()->twitch.getCurrent()->getUserName(); if (senderLogin == currentLogin) { thread->markSubscribed(); - // don't set the highlight here + } + else if (const auto it = tags.find("reply-parent-user-login"); + it != tags.end()) + { + auto name = it.value().toString(); + if (name == currentLogin) + { + thread->markSubscribed(); + } } } - - return false; } ChannelPtr channelOrEmptyByTarget(const QString &target, @@ -243,7 +230,6 @@ QMap parseBadges(const QString &badgesString) struct ReplyContext { std::shared_ptr thread; MessagePtr parent; - bool highlight = false; }; [[nodiscard]] ReplyContext getReplyContext( @@ -265,8 +251,7 @@ struct ReplyContext { if (owned) { // Thread already exists (has a reply) - ctx.highlight = shouldHighlightReplyThread( - tags, message->nick(), owned, false); + checkThreadSubscription(tags, message->nick(), owned); ctx.thread = owned; rootThread = owned; } @@ -301,8 +286,7 @@ struct ReplyContext { { std::shared_ptr newThread = std::make_shared(foundMessage); - ctx.highlight = shouldHighlightReplyThread( - tags, message->nick(), newThread, true); + checkThreadSubscription(tags, message->nick(), newThread); ctx.thread = newThread; rootThread = newThread; @@ -724,10 +708,6 @@ std::vector IrcMessageHandler::parseMessageWithReply( if (built) { - if (replyCtx.highlight) - { - built->flags.set(MessageFlag::SubscribedThread); - } builtMessages.emplace_back(built); MessageBuilder::triggerHighlights(channel, alert); } @@ -1552,8 +1532,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, { // Thread already exists (has a reply) auto thread = threadIt->second.lock(); - replyCtx.highlight = shouldHighlightReplyThread( - tags, message->nick(), thread, false); + checkThreadSubscription(tags, message->nick(), thread); replyCtx.thread = thread; rootThread = thread; } @@ -1565,8 +1544,7 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, { // Found root reply message auto newThread = std::make_shared(root); - replyCtx.highlight = shouldHighlightReplyThread( - tags, message->nick(), newThread, true); + checkThreadSubscription(tags, message->nick(), newThread); replyCtx.thread = newThread; rootThread = newThread; @@ -1621,10 +1599,6 @@ void IrcMessageHandler::addMessage(Communi::IrcMessage *message, msg->flags.set(MessageFlag::Subscription); msg->flags.unset(MessageFlag::Highlighted); } - if (replyCtx.highlight) - { - msg->flags.set(MessageFlag::SubscribedThread); - } IrcMessageHandler::setSimilarityFlags(msg, chan); From 18c4815ad74bc794b952a3a787d59477952b91a7 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:42:19 +0000 Subject: [PATCH 5/6] feat: add shared chat badge (#5661) --- CHANGELOG.md | 2 +- mocks/include/mocks/BaseApplication.hpp | 7 ++++ mocks/include/mocks/TwitchUsers.hpp | 24 ++++++++++++ resources/twitch/sharedChat.png | Bin 0 -> 1026 bytes src/messages/MessageBuilder.cpp | 35 ++++++++++++++++++ src/messages/MessageElement.hpp | 6 ++- src/singletons/WindowManager.cpp | 1 + src/widgets/helper/ChannelView.cpp | 5 +++ .../shared-chat-announcement.json | 18 +++++++++ .../IrcMessageHandler/shared-chat-emotes.json | 18 +++++++++ .../IrcMessageHandler/shared-chat-known.json | 18 +++++++++ .../shared-chat-unknown.json | 18 +++++++++ 12 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 mocks/include/mocks/TwitchUsers.hpp create mode 100644 resources/twitch/sharedChat.png diff --git a/CHANGELOG.md b/CHANGELOG.md index c95aa2df7..7497e46dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Major: Improve high-DPI support on Windows. (#4868, #5391, #5664, #5666) - Major: Added transparent overlay window (default keybind: CTRL + ALT + N). (#4746, #5643, #5659) - Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530) -- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625) +- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606, #5625, #5661) - Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530) - Minor: Add option to customise Moderation buttons with images. (#5369) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) diff --git a/mocks/include/mocks/BaseApplication.hpp b/mocks/include/mocks/BaseApplication.hpp index 2ba9f949c..619203afc 100644 --- a/mocks/include/mocks/BaseApplication.hpp +++ b/mocks/include/mocks/BaseApplication.hpp @@ -3,6 +3,7 @@ #include "common/Args.hpp" #include "mocks/DisabledStreamerMode.hpp" #include "mocks/EmptyApplication.hpp" +#include "mocks/TwitchUsers.hpp" #include "providers/bttv/BttvLiveUpdates.hpp" #include "singletons/Fonts.hpp" #include "singletons/Settings.hpp" @@ -55,6 +56,11 @@ public: return &this->fonts; } + ITwitchUsers *getTwitchUsers() override + { + return &this->twitchUsers; + } + BttvLiveUpdates *getBttvLiveUpdates() override { return nullptr; @@ -71,6 +77,7 @@ public: DisabledStreamerMode streamerMode; Theme theme; Fonts fonts; + TwitchUsers twitchUsers; }; } // namespace chatterino::mock diff --git a/mocks/include/mocks/TwitchUsers.hpp b/mocks/include/mocks/TwitchUsers.hpp new file mode 100644 index 000000000..14f6bf7e6 --- /dev/null +++ b/mocks/include/mocks/TwitchUsers.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "providers/twitch/TwitchUser.hpp" +#include "providers/twitch/TwitchUsers.hpp" + +namespace chatterino::mock { + +class TwitchUsers : public ITwitchUsers +{ +public: + TwitchUsers() = default; + + std::shared_ptr resolveID(const UserId &id) + { + TwitchUser u = { + .id = id.string, + .name = {}, + .displayName = {}, + }; + return std::make_shared(u); + } +}; + +} // namespace chatterino::mock diff --git a/resources/twitch/sharedChat.png b/resources/twitch/sharedChat.png new file mode 100644 index 0000000000000000000000000000000000000000..f9a66b17c8f1307a811337d4095cfaf1d5133515 GIT binary patch literal 1026 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAk#;Q*fy*NU|NG(!LX|Nr^(e@*uP zd$<4p{rms?ng1U@{9iif|L@=buU-Cs`TYM?i~qlU^Z)U~|I@nvSEm0zxaa@IRsSb6 z|9}4M|DnDA`x^h>xcYzj{QnbM|9}7X|M;Q*i)Q|xH}(Il{{MA(|DQei-(LCu#q<=zrK9*{>1C^clU0Xn)vbhy!sVAg*UfUI2;J^GsxzPd&a=P zbkWnrF(l&f+Z#8_RvCz}J>ckI4mh6Gp1eq8rcA-QsO;bW{7YvAziCqwl*&8j`oBAl z;kEY6ZQZ^9?nM4g+q+Op)#OKEfM=le*R<1Bdz{PpV=u4BdMY%jF<|AI&}yKVL)iAq z*Rn-j#e%dR$gQXeipB1Fb4wiI2 z)>(dif#tKbx}Ez~3nu2QcM!HS*|~?m)L~YaVmrU($G6wNJY6cGRMTu|=CWKQEhf`u($Zu_j(ksaa11KN_h^Z&GV=o?ZM?@%@Y-&FVjXfAll)^oi9wdK|>goDW=gNNsxji~kMF<|(h3uuOo{TaS6teGKZnoa~OJoN)-YzWg&zh?9*|LBGHGu!>011cstd z7EdO$@Uf^g+_5=l@$$e`5tTVg`cFGl8s@Q{yFH<>LdZdwQRMIJ?w$$E4%%nV&s^u1 z=G3rk#(d$s`ZE<7I*)yGDlbU;!&35OowTjU;fv2t);@Yte7&cQq4VFy>Lo%p^SJJK zI2~3Euc}K_xyIP>?9VjsB-v$7^Y-YR^mnqe@L74c%_h^RTXkDcgRw__*SV&Nv%^kk zG&CBdvfN`gls~>aVLzvA((NOh=D#0&F;%tW>_5rgulTY3gV~p2*(FT#Ute&T;CII% zBV%e((49*^vNnknYTa7fH_LYKGA#v9%d6GQTYZ?NqBi;M%~e{R`fb5cHO8qcFTY&u zzFO4$?}U`}H{bT%eHpdo^>xE9O0o8L4SrvH93SN4boPM9;wQjdz~JfX=d#Wzp$Pys C^A0Qk literal 0 HcmV?d00001 diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index a17f955a9..601ba8ec9 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -32,6 +32,7 @@ #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrc.hpp" #include "providers/twitch/TwitchIrcServer.hpp" +#include "providers/twitch/TwitchUsers.hpp" #include "singletons/Emotes.hpp" #include "singletons/Resources.hpp" #include "singletons/Settings.hpp" @@ -380,6 +381,18 @@ EmotePtr makeAutoModBadge() Url{"https://dashboard.twitch.tv/settings/moderation/automod"}}); } +EmotePtr makeSharedChatBadge(const QString &sourceName) +{ + return std::make_shared(Emote{ + .name = EmoteName{}, + .images = ImageSet{Image::fromResourcePixmap( + getResources().twitch.sharedChat, 0.25)}, + .tooltip = Tooltip{"Shared Message" + + (sourceName.isEmpty() ? "" : " from " + sourceName)}, + .homePage = Url{"https://link.twitch.tv/SharedChatViewer"}, + }); +} + std::tuple, MessageElementFlags, bool> parseEmote( TwitchChannel *twitchChannel, const EmoteName &name) { @@ -2751,6 +2764,28 @@ void MessageBuilder::appendTwitchBadges(const QVariantMap &tags, return; } + if (this->message().flags.has(MessageFlag::SharedMessage)) + { + const QString sourceId = tags["source-room-id"].toString(); + QString sourceName; + if (sourceId.isEmpty()) + { + sourceName = ""; + } + else if (twitchChannel->roomId() == sourceId) + { + sourceName = twitchChannel->getName(); + } + else + { + sourceName = + getApp()->getTwitchUsers()->resolveID({sourceId})->displayName; + } + + this->emplace(makeSharedChatBadge(sourceName), + MessageElementFlag::BadgeSharedChannel); + } + auto badgeInfos = parseBadgeInfoTag(tags); auto badges = parseBadgeTag(tags); appendBadges(this, badges, badgeInfos, twitchChannel); diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index 0e6c07b93..c19ed5c0c 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -66,6 +66,10 @@ enum class MessageElementFlag : int64_t { BitsStatic = (1LL << 11), BitsAnimated = (1LL << 12), + // Slot 0: Twitch + // - Shared Channel indicator badge + BadgeSharedChannel = (1LL << 37), + // Slot 1: Twitch // - Staff badge // - Admin badge @@ -119,7 +123,7 @@ enum class MessageElementFlag : int64_t { Badges = BadgeGlobalAuthority | BadgePredictions | BadgeChannelAuthority | BadgeSubscription | BadgeVanity | BadgeChatterino | BadgeSevenTV | - BadgeFfz, + BadgeFfz | BadgeSharedChannel, ChannelName = (1LL << 20), diff --git a/src/singletons/WindowManager.cpp b/src/singletons/WindowManager.cpp index f64eb47e7..88d5ec565 100644 --- a/src/singletons/WindowManager.cpp +++ b/src/singletons/WindowManager.cpp @@ -195,6 +195,7 @@ void WindowManager::updateWordTypeMask() flags.set(settings->animateEmotes ? MEF::BitsAnimated : MEF::BitsStatic); // badges + flags.set(MEF::BadgeSharedChannel); flags.set(settings->showBadgesGlobalAuthority ? MEF::BadgeGlobalAuthority : MEF::None); flags.set(settings->showBadgesPredictions ? MEF::BadgePredictions diff --git a/src/widgets/helper/ChannelView.cpp b/src/widgets/helper/ChannelView.cpp index 066085030..50f6d1acc 100644 --- a/src/widgets/helper/ChannelView.cpp +++ b/src/widgets/helper/ChannelView.cpp @@ -2408,6 +2408,11 @@ void ChannelView::handleMouseClick(QMouseEvent *event, return; } + if (link.value.startsWith("id:")) + { + return; + } + // Insert @username into split input const bool commaMention = getSettings()->mentionUsersWithComma; diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json b/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json index 9c6fa2f65..a6877c365 100644 --- a/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json +++ b/tests/snapshots/IrcMessageHandler/shared-chat-announcement.json @@ -64,6 +64,24 @@ "trailingSpace": true, "type": "TwitchModerationElement" }, + { + "emote": { + "homePage": "https://link.twitch.tv/SharedChatViewer", + "images": { + "1x": "" + }, + "name": "", + "tooltip": "Shared Message" + }, + "flags": "BadgeSharedChannel", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Shared Message", + "trailingSpace": true, + "type": "BadgeElement" + }, { "emote": { "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json b/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json index b7f9256d4..ba6485c21 100644 --- a/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json +++ b/tests/snapshots/IrcMessageHandler/shared-chat-emotes.json @@ -64,6 +64,24 @@ "trailingSpace": true, "type": "TwitchModerationElement" }, + { + "emote": { + "homePage": "https://link.twitch.tv/SharedChatViewer", + "images": { + "1x": "" + }, + "name": "", + "tooltip": "Shared Message from twitchdev" + }, + "flags": "BadgeSharedChannel", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Shared Message from twitchdev", + "trailingSpace": true, + "type": "BadgeElement" + }, { "color": "#ffff0000", "flags": "Username", diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-known.json b/tests/snapshots/IrcMessageHandler/shared-chat-known.json index 6a13adcb4..c4a3f3e2f 100644 --- a/tests/snapshots/IrcMessageHandler/shared-chat-known.json +++ b/tests/snapshots/IrcMessageHandler/shared-chat-known.json @@ -64,6 +64,24 @@ "trailingSpace": true, "type": "TwitchModerationElement" }, + { + "emote": { + "homePage": "https://link.twitch.tv/SharedChatViewer", + "images": { + "1x": "" + }, + "name": "", + "tooltip": "Shared Message from twitchdev" + }, + "flags": "BadgeSharedChannel", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Shared Message from twitchdev", + "trailingSpace": true, + "type": "BadgeElement" + }, { "emote": { "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json b/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json index accbb76b0..6ea80b2ce 100644 --- a/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json +++ b/tests/snapshots/IrcMessageHandler/shared-chat-unknown.json @@ -64,6 +64,24 @@ "trailingSpace": true, "type": "TwitchModerationElement" }, + { + "emote": { + "homePage": "https://link.twitch.tv/SharedChatViewer", + "images": { + "1x": "" + }, + "name": "", + "tooltip": "Shared Message" + }, + "flags": "BadgeSharedChannel", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "Shared Message", + "trailingSpace": true, + "type": "BadgeElement" + }, { "emote": { "homePage": "https://www.twitch.tv/jobs?ref=chat_badge", From 55a12fa00838e7c6a859e14d388fe2bcb42bcfaf Mon Sep 17 00:00:00 2001 From: nerix Date: Tue, 22 Oct 2024 23:06:29 +0200 Subject: [PATCH 6/6] test: add snapshots for raids, timeouts, and notices (#5671) --- CHANGELOG.md | 2 +- .../IrcMessageHandler/clearchat.json | 157 ++++++++++++++++++ .../IrcMessageHandler/emoteonly-on.json | 157 ++++++++++++++++++ tests/snapshots/IrcMessageHandler/raid.json | 145 ++++++++++++++++ .../IrcMessageHandler/shared-chat-raid.json | 5 + .../snapshots/IrcMessageHandler/timeout.json | 87 ++++++++++ 6 files changed, 552 insertions(+), 1 deletion(-) create mode 100644 tests/snapshots/IrcMessageHandler/clearchat.json create mode 100644 tests/snapshots/IrcMessageHandler/emoteonly-on.json create mode 100644 tests/snapshots/IrcMessageHandler/raid.json create mode 100644 tests/snapshots/IrcMessageHandler/shared-chat-raid.json create mode 100644 tests/snapshots/IrcMessageHandler/timeout.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 7497e46dc..2992cd350 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,7 +104,7 @@ - Dev: Added more tests for input completion. (#5604) - Dev: Refactored legacy Unicode zero-width-joiner replacement. (#5594) - Dev: The JSON output when copying a message (SHIFT + right-click) is now more extensive. (#5600) -- Dev: Added more tests for message building. (#5598, #5654, #5656) +- Dev: Added more tests for message building. (#5598, #5654, #5656, #5671) - Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607) - Dev: `GIFTimer` is no longer initialized in tests. (#5608) - Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616) diff --git a/tests/snapshots/IrcMessageHandler/clearchat.json b/tests/snapshots/IrcMessageHandler/clearchat.json new file mode 100644 index 000000000..d40f9e704 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/clearchat.json @@ -0,0 +1,157 @@ +{ + "input": "@room-id=11148817;rm-received-ts=1729627607652;tmi-sent-ts=1729627607545;historical=1 :tmi.twitch.tv CLEARCHAT #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:06" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:06:47", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Chat" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "has" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "been" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "cleared" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "by" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "a" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "moderator." + ] + } + ], + "flags": "System|DoNotTriggerNotification", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "Chat has been cleared by a moderator.", + "searchText": "Chat has been cleared by a moderator.", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/emoteonly-on.json b/tests/snapshots/IrcMessageHandler/emoteonly-on.json new file mode 100644 index 000000000..d3e0c4980 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/emoteonly-on.json @@ -0,0 +1,157 @@ +{ + "input": "@historical=1;rm-received-ts=1729627965650;msg-id=emote_only_on :tmi.twitch.tv NOTICE #pajlada :This room is now in emote-only mode.", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:12" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:12:45", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "This" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "room" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "is" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "now" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "in" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "emote-only" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "mode." + ] + } + ], + "flags": "System|DoNotTriggerNotification", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "This room is now in emote-only mode.", + "searchText": "This room is now in emote-only mode.", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/raid.json b/tests/snapshots/IrcMessageHandler/raid.json new file mode 100644 index 000000000..1e4e84d42 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/raid.json @@ -0,0 +1,145 @@ +{ + "input": "@badges=subscriber/24;login=nerixyz;msg-param-displayName=nerixyz;user-type=;tmi-sent-ts=1729626466361;system-msg=2\\sraiders\\sfrom\\snerixyz\\shave\\sjoined!;room-id=11148817;user-id=129546453;display-name=nerixyz;subscriber=1;historical=1;rm-received-ts=1729626466492;msg-id=raid;vip=0;id=7299b7bc-61ce-423c-85ce-8d651b56cce4;msg-param-login=nerixyz;color=#FF0000;mod=0;msg-param-viewerCount=2;flags=;msg-param-profileImageURL=https://static-cdn.jtvnw.net/jtv_user_pictures/e065218b-49df-459d-afd3-c6557870f551-profile_image-%s.png;emotes=;badge-info=subscriber/28 :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "19:47" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "19:47:46", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "2" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "raiders" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "from" + ] + }, + { + "color": "Text", + "fallbackColor": "System", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "#ffff0000", + "userLoginName": "nerixyz", + "words": [ + "nerixyz" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "have" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "joined!" + ] + } + ], + "flags": "System|DoNotTriggerNotification|Subscription", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "2 raiders from nerixyz have joined!", + "searchText": "2 raiders from nerixyz have joined!", + "serverReceivedTime": "", + "timeoutUser": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/IrcMessageHandler/shared-chat-raid.json b/tests/snapshots/IrcMessageHandler/shared-chat-raid.json new file mode 100644 index 000000000..d3edb3741 --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/shared-chat-raid.json @@ -0,0 +1,5 @@ +{ + "input": "@color=#FF0000;emotes=;subscriber=0;msg-id=sharedchatnotice;historical=1;msg-param-profileImageURL=https://static-cdn.jtvnw.net/jtv_user_pictures/e065218b-49df-459d-afd3-c6557870f551-profile_image-%s.png;tmi-sent-ts=1729627237027;rm-received-ts=1729627237138;msg-param-displayName=nerixyz;id=c585cb3e-cb4f-4a48-a251-b568d217587e;display-name=nerixyz;badges=;user-id=129546453;source-id=d86cdfb2-e138-48e2-985f-5b8efb765ba4;source-room-id=955766119;room-id=11148817;user-type=;msg-param-login=nerixyz;flags=;source-badge-info=;mod=0;vip=0;system-msg=2\\sraiders\\sfrom\\snerixyz\\shave\\sjoined!;login=nerixyz;msg-param-viewerCount=2;source-badges=;source-msg-id=raid;badge-info= :tmi.twitch.tv USERNOTICE #pajlada", + "output": [ + ] +} diff --git a/tests/snapshots/IrcMessageHandler/timeout.json b/tests/snapshots/IrcMessageHandler/timeout.json new file mode 100644 index 000000000..1de84089d --- /dev/null +++ b/tests/snapshots/IrcMessageHandler/timeout.json @@ -0,0 +1,87 @@ +{ + "input": "@tmi-sent-ts=1729628658012;rm-received-ts=1729628658106;historical=1;ban-duration=1;room-id=11148817;target-user-id=129546453 :tmi.twitch.tv CLEARCHAT #pajlada nerixyz", + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "", + "count": 1, + "displayName": "", + "elements": [ + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "20:24" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "20:24:18", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "UserInfo", + "value": "nerixyz" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "nerixyz" + ] + }, + { + "color": "System", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "has", + "been", + "timed", + "out", + "for", + "1s." + ] + } + ], + "flags": "System|Timeout|DoNotTriggerNotification", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "nerixyz has been timed out for 1s. ", + "searchText": "nerixyz has been timed out for 1s. ", + "serverReceivedTime": "", + "timeoutUser": "nerixyz", + "usernameColor": "#ff000000" + } + ] +}