refactor: irc message builder (#5663)

This commit is contained in:
nerix 2024-10-20 12:40:48 +02:00 committed by GitHub
parent 352a4ec132
commit e35fabfabe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 703 additions and 804 deletions

View file

@ -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

View file

@ -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

View file

@ -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 <benchmark/benchmark.h>
#include <QDebug>
#include <QString>
#include <QTemporaryDir>
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<Communi::IrcPrivateMessage *>(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);

View file

@ -22,6 +22,7 @@ class ScrollbarHighlight;
struct Message;
using MessagePtr = std::shared_ptr<const Message>;
using MessagePtrMut = std::shared_ptr<Message>;
struct Message {
Message();
~Message();

File diff suppressed because it is too large Load diff

View file

@ -14,8 +14,6 @@
#include <ctime>
#include <memory>
#include <optional>
#include <tuple>
#include <unordered_map>
#include <utility>
@ -31,6 +29,7 @@ struct AutomodUserAction;
struct AutomodInfoAction;
struct Message;
using MessagePtr = std::shared_ptr<const Message>;
using MessagePtrMut = std::shared_ptr<Message>;
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<Message> weakOf();
MessagePtrMut release();
std::weak_ptr<const Message> weakOf();
void append(std::unique_ptr<MessageElement> 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<MessageThread> 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<MessagePtrMut, HighlightAlert> makeIrcMessage(
Channel *channel, const Communi::IrcMessage *ircMessage,
const MessageParseArgs &args, QString content,
QString::size_type messageOffset,
const std::shared_ptr<MessageThread> &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<MessageElement> 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> 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<MessageThread> &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<std::optional<EmotePtr>, 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<TwitchEmoteOccurrence> &twitchEmotes);
const std::vector<TwitchEmoteOccurrence> &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<MessageThread> 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> message_;
MessageColor textColor_ = MessageColor::Text;
QColor usernameColor_ = {153, 153, 153};
bool highlightAlert_ = false;
bool highlightSound_ = false;
std::optional<QUrl> highlightSoundCustomUrl_{};
};
} // namespace chatterino

View file

@ -508,15 +508,20 @@ std::vector<MessagePtr> 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<MessagePtr> parsePrivMessage(Channel *channel,
std::vector<MessagePtr> 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<MessagePtr> 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<TwitchChannel *>(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);

View file

@ -153,7 +153,7 @@
"searchText": "mm2pl mm2pl: Kappa ",
"serverReceivedTime": "2022-09-03T10:31:42Z",
"timeoutUser": "",
"usernameColor": "#ff000000"
"usernameColor": "#ffdaa521"
}
]
}

View file

@ -153,7 +153,7 @@
"searchText": "mm2pl mm2pl: Kappa ",
"serverReceivedTime": "2022-09-03T10:31:42Z",
"timeoutUser": "",
"usernameColor": "#ff000000"
"usernameColor": "#ffdaa521"
}
]
}

View file

@ -153,7 +153,7 @@
"searchText": "mm2pl mm2pl: Keepo ",
"serverReceivedTime": "2022-09-03T10:31:35Z",
"timeoutUser": "",
"usernameColor": "#ff000000"
"usernameColor": "#ffdaa521"
}
]
}

View file

@ -221,7 +221,7 @@
"searchText": "mm2pl mm2pl: Kappa Keepo PogChamp ",
"serverReceivedTime": "2022-09-03T10:31:42Z",
"timeoutUser": "",
"usernameColor": "#ff000000"
"usernameColor": "#ffdaa521"
}
]
}

View file

@ -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);