refactor: trim down PubSub implementation (#6158)

This commit is contained in:
nerix
2025-04-19 14:31:22 +02:00
committed by GitHub
parent 52067d0af7
commit 9d557146b4
24 changed files with 20 additions and 3014 deletions
-650
View File
@@ -25,8 +25,6 @@
#include "providers/seventv/SeventvEmotes.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/pubsubmessages/AutoMod.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include "providers/twitch/TwitchBadges.hpp"
@@ -376,15 +374,6 @@ bool doesWordContainATwitchEmote(
return true;
}
EmotePtr makeAutoModBadge()
{
return std::make_shared<Emote>(Emote{
EmoteName{},
ImageSet{Image::fromResourcePixmap(getResources().twitch.automod)},
Tooltip{"AutoMod"},
Url{"https://dashboard.twitch.tv/settings/moderation/automod"}});
}
EmotePtr makeSharedChatBadge(const QString &sourceName,
const QString &sourceProfileURL,
const QString &sourceLogin)
@@ -773,239 +762,6 @@ MessageBuilder::MessageBuilder(TimeoutMessageTag, const QString &username,
this->message().serverReceivedTime = time;
}
MessageBuilder::MessageBuilder(const BanAction &action, const QDateTime &time,
uint32_t count)
: MessageBuilder()
{
auto current = getApp()->getAccounts()->twitch.getCurrent();
this->emplace<TimestampElement>();
this->message().flags.set(MessageFlag::System);
this->message().flags.set(MessageFlag::Timeout);
this->message().flags.set(MessageFlag::ModerationAction);
this->message().timeoutUser = action.target.login;
this->message().loginName = action.source.login;
this->message().count = count;
QString text;
if (action.target.id == current->getUserId())
{
this->emplaceSystemTextAndUpdate("You", text)
->setLink({Link::UserInfo, current->getUserName()});
this->emplaceSystemTextAndUpdate("were", text);
if (action.isBan())
{
this->appendOrEmplaceSystemTextAndUpdate("banned", text);
}
else
{
this->appendOrEmplaceSystemTextAndUpdate(
QString("timed out for %1").arg(formatTime(action.duration)),
text);
}
if (!action.source.login.isEmpty())
{
this->appendOrEmplaceSystemTextAndUpdate("by", text);
this->emplaceSystemTextAndUpdate(
action.source.login + (action.reason.isEmpty() ? "." : ":"),
text)
->setLink({Link::UserInfo, action.source.login});
}
if (!action.reason.isEmpty())
{
this->appendOrEmplaceSystemTextAndUpdate(
QString("\"%1\".").arg(action.reason), text);
}
}
else
{
if (action.isBan())
{
this->emplaceSystemTextAndUpdate(action.source.login, text)
->setLink({Link::UserInfo, action.source.login});
this->emplaceSystemTextAndUpdate("banned", text);
if (action.reason.isEmpty())
{
this->emplaceSystemTextAndUpdate(action.target.login + ".",
text)
->setLink({Link::UserInfo, action.target.login});
}
else
{
this->emplaceSystemTextAndUpdate(action.target.login + ":",
text)
->setLink({Link::UserInfo, action.target.login});
this->emplaceSystemTextAndUpdate(
QString("\"%1\".").arg(action.reason), text);
}
}
else
{
this->emplaceSystemTextAndUpdate(action.source.login, text)
->setLink({Link::UserInfo, action.source.login});
this->emplaceSystemTextAndUpdate("timed out", text);
this->emplaceSystemTextAndUpdate(action.target.login, text)
->setLink({Link::UserInfo, action.target.login});
if (action.reason.isEmpty())
{
this->emplaceSystemTextAndUpdate(
QString("for %1.").arg(formatTime(action.duration)), text);
}
else
{
this->emplaceSystemTextAndUpdate(
QString("for %1: \"%2\".")
.arg(formatTime(action.duration))
.arg(action.reason),
text);
}
if (count > 1)
{
this->appendOrEmplaceSystemTextAndUpdate(
QString("(%1 times)").arg(count), text);
}
}
}
this->message().messageText = text;
this->message().searchText = text;
this->message().serverReceivedTime = time;
}
MessageBuilder::MessageBuilder(const UnbanAction &action, const QDateTime &time)
: MessageBuilder()
{
this->emplace<TimestampElement>();
this->message().flags.set(MessageFlag::System);
this->message().flags.set(MessageFlag::Untimeout);
this->message().timeoutUser = action.target.login;
QString text;
this->emplaceSystemTextAndUpdate(action.source.login, text)
->setLink({Link::UserInfo, action.source.login});
this->emplaceSystemTextAndUpdate(
action.wasBan() ? "unbanned" : "untimedout", text);
this->emplaceSystemTextAndUpdate(action.target.login + ".", text)
->setLink({Link::UserInfo, action.target.login});
this->message().messageText = text;
this->message().searchText = text;
this->message().serverReceivedTime = time;
}
MessageBuilder::MessageBuilder(const WarnAction &action)
: MessageBuilder()
{
this->emplace<TimestampElement>();
this->message().flags.set(MessageFlag::System);
QString text;
// TODO: Use MentionElement here, once WarnAction includes username/displayname
this->emplaceSystemTextAndUpdate("A moderator", text)
->setLink({Link::UserInfo, "id:" + action.source.id});
this->emplaceSystemTextAndUpdate("warned", text);
this->emplaceSystemTextAndUpdate(
action.target.login + (action.reasons.isEmpty() ? "." : ":"), text)
->setLink({Link::UserInfo, action.target.login});
if (!action.reasons.isEmpty())
{
this->emplaceSystemTextAndUpdate(action.reasons.join(", "), text);
}
this->message().messageText = text;
this->message().searchText = text;
}
MessageBuilder::MessageBuilder(const RaidAction &action)
: MessageBuilder()
{
this->emplace<TimestampElement>();
this->message().flags.set(MessageFlag::System);
QString text;
this->emplaceSystemTextAndUpdate(action.source.login, text)
->setLink({Link::UserInfo, "id:" + action.source.id});
this->emplaceSystemTextAndUpdate("initiated a raid to", text);
this->emplaceSystemTextAndUpdate(action.target + ".", text)
->setLink({Link::UserInfo, action.target});
this->message().messageText = text;
this->message().searchText = text;
}
MessageBuilder::MessageBuilder(const UnraidAction &action)
: MessageBuilder()
{
this->emplace<TimestampElement>();
this->message().flags.set(MessageFlag::System);
QString text;
this->emplaceSystemTextAndUpdate(action.source.login, text)
->setLink({Link::UserInfo, "id:" + action.source.id});
this->emplaceSystemTextAndUpdate("canceled the raid.", text);
this->message().messageText = text;
this->message().searchText = text;
}
MessageBuilder::MessageBuilder(const AutomodUserAction &action)
: MessageBuilder()
{
this->emplace<TimestampElement>();
this->message().flags.set(MessageFlag::System);
QString text;
switch (action.type)
{
case AutomodUserAction::AddPermitted: {
text = QString("%1 added \"%2\" as a permitted term on AutoMod.")
.arg(action.source.login, action.message);
}
break;
case AutomodUserAction::AddBlocked: {
text = QString("%1 added \"%2\" as a blocked term on AutoMod.")
.arg(action.source.login, action.message);
}
break;
case AutomodUserAction::RemovePermitted: {
text = QString("%1 removed \"%2\" as a permitted term on AutoMod.")
.arg(action.source.login, action.message);
}
break;
case AutomodUserAction::RemoveBlocked: {
text = QString("%1 removed \"%2\" as a blocked term on AutoMod.")
.arg(action.source.login, action.message);
}
break;
case AutomodUserAction::Properties: {
text = QString("%1 modified the AutoMod properties.")
.arg(action.source.login);
}
break;
}
this->message().messageText = text;
this->message().searchText = text;
this->emplace<TextElement>(text, MessageElementFlag::Text,
MessageColor::System);
}
MessageBuilder::MessageBuilder(LiveUpdatesAddEmoteMessageTag /*unused*/,
const QString &platform, const QString &actor,
const std::vector<QString> &emoteNames)
@@ -1531,50 +1287,6 @@ MessagePtr MessageBuilder::makeDeletionMessageFromIRC(
return builder.release();
}
MessagePtr MessageBuilder::makeDeletionMessageFromPubSub(
const DeleteAction &action)
{
MessageBuilder builder;
builder.emplace<TimestampElement>();
builder.message().flags.set(MessageFlag::System);
builder.message().flags.set(MessageFlag::DoNotTriggerNotification);
builder.message().flags.set(MessageFlag::ModerationAction);
builder
.emplace<TextElement>(action.source.login, MessageElementFlag::Username,
MessageColor::System, FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, action.source.login});
// TODO(mm2pl): If or when jumping to a single message gets implemented a link,
// add a link to the originalMessage
builder.emplace<TextElement>(
"deleted message from", MessageElementFlag::Text, MessageColor::System);
builder
.emplace<TextElement>(action.target.login, MessageElementFlag::Username,
MessageColor::System, FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, action.target.login});
builder.emplace<TextElement>("saying:", MessageElementFlag::Text,
MessageColor::System);
if (action.messageText.length() > 50)
{
builder
.emplace<TextElement>(action.messageText.left(50) + "",
MessageElementFlag::Text, MessageColor::Text)
->setLink({Link::JumpToMessage, action.messageId});
}
else
{
builder
.emplace<TextElement>(action.messageText, MessageElementFlag::Text,
MessageColor::Text)
->setLink({Link::JumpToMessage, action.messageId});
}
builder.message().timeoutUser = "msg:" + action.messageId;
builder.message().flags.set(MessageFlag::PubSub);
return builder.release();
}
MessagePtr MessageBuilder::makeListOfUsersMessage(QString prefix,
QStringList users,
Channel *channel,
@@ -1729,368 +1441,6 @@ MessagePtr MessageBuilder::buildHypeChatMessage(
return builder.release();
}
std::pair<MessagePtr, MessagePtr> MessageBuilder::makeAutomodMessage(
const AutomodAction &action, const QString &channelName)
{
MessageBuilder builder, builder2;
if (action.reasonCode == PubSubAutoModQueueMessage::Reason::BlockedTerm)
{
builder.message().flags.set(MessageFlag::AutoModBlockedTerm);
builder2.message().flags.set(MessageFlag::AutoModBlockedTerm);
}
//
// Builder for AutoMod message with explanation
builder.message().id = "automod_" + action.msgID;
builder.message().loginName = "automod";
builder.message().channelName = channelName;
builder.message().flags.set(MessageFlag::PubSub);
builder.message().flags.set(MessageFlag::ModerationAction);
builder.message().flags.set(MessageFlag::AutoMod);
builder.message().flags.set(MessageFlag::AutoModOffendingMessageHeader);
// AutoMod shield badge
builder.emplace<BadgeElement>(makeAutoModBadge(),
MessageElementFlag::BadgeChannelAuthority);
// AutoMod "username"
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::Text,
AUTOMOD_USER_COLOR, FontStyle::ChatMediumBold);
// AutoMod header message
builder.emplace<TextElement>(
("Held a message for reason: " + action.reason +
". Allow will post it in chat. "),
MessageElementFlag::Text, MessageColor::Text);
// Allow link button
builder
.emplace<TextElement>("Allow", MessageElementFlag::Text,
MessageColor(QColor("green")),
FontStyle::ChatMediumBold)
->setLink({Link::AutoModAllow, action.msgID});
// Deny link button
builder
.emplace<TextElement>(" Deny", MessageElementFlag::Text,
MessageColor(QColor("red")),
FontStyle::ChatMediumBold)
->setLink({Link::AutoModDeny, action.msgID});
// ID of message caught by AutoMod
// builder.emplace<TextElement>(action.msgID, MessageElementFlag::Text,
// MessageColor::Text);
auto text1 =
QString("AutoMod: Held a message for reason: %1. Allow will post "
"it in chat. Allow Deny")
.arg(action.reason);
builder.message().messageText = text1;
builder.message().searchText = text1;
auto message1 = builder.release();
//
// Builder for offender's message
builder2.message().channelName = channelName;
builder2
.emplace<TextElement>("#" + channelName,
MessageElementFlag::ChannelName,
MessageColor::System)
->setLink({Link::JumpToChannel, channelName});
builder2.emplace<TimestampElement>();
builder2.emplace<TwitchModerationElement>();
builder2.message().loginName = action.target.login;
builder2.message().flags.set(MessageFlag::PubSub);
builder2.message().flags.set(MessageFlag::ModerationAction);
builder2.message().flags.set(MessageFlag::AutoMod);
builder2.message().flags.set(MessageFlag::AutoModOffendingMessage);
// sender username
builder2.emplace<MentionElement>(action.target.displayName + ":",
action.target.login, MessageColor::Text,
action.target.color);
// sender's message caught by AutoMod
builder2.emplace<TextElement>(action.message, MessageElementFlag::Text,
MessageColor::Text);
auto text2 =
QString("%1: %2").arg(action.target.displayName, action.message);
builder2.message().messageText = text2;
builder2.message().searchText = text2;
auto message2 = builder2.release();
// Normally highlights would be checked & triggered during the builder parse steps
// and when the message is added to the channel
// We do this a bit weird since the message comes in from PubSub and not the normal message route
auto [highlighted, highlightResult] = getApp()->getHighlights()->check(
{}, {}, action.target.login, action.message, message2->flags);
if (highlighted)
{
actuallyTriggerHighlights(
channelName, highlightResult.playSound,
highlightResult.customSoundUrl.value_or(QUrl{}),
highlightResult.alert);
}
return std::make_pair(message1, message2);
}
MessagePtr MessageBuilder::makeAutomodInfoMessage(
const AutomodInfoAction &action)
{
auto builder = MessageBuilder();
QString text("AutoMod: ");
builder.emplace<TimestampElement>();
builder.message().flags.set(MessageFlag::PubSub);
// AutoMod shield badge
builder.emplace<BadgeElement>(makeAutoModBadge(),
MessageElementFlag::BadgeChannelAuthority);
// AutoMod "username"
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::Text,
AUTOMOD_USER_COLOR, FontStyle::ChatMediumBold);
switch (action.type)
{
case AutomodInfoAction::OnHold: {
QString info("Hey! Your message is being checked "
"by mods and has not been sent.");
text += info;
builder.appendOrEmplaceText(info, MessageColor::Text);
}
break;
case AutomodInfoAction::Denied: {
QString info("Mods have removed your message.");
text += info;
builder.appendOrEmplaceText(info, MessageColor::Text);
}
break;
case AutomodInfoAction::Approved: {
QString info("Mods have accepted your message.");
text += info;
builder.appendOrEmplaceText(info, MessageColor::Text);
}
break;
}
builder.message().flags.set(MessageFlag::AutoMod);
builder.message().messageText = text;
builder.message().searchText = text;
auto message = builder.release();
return message;
}
std::pair<MessagePtr, MessagePtr> MessageBuilder::makeLowTrustUserMessage(
const PubSubLowTrustUsersMessage &action, const QString &channelName,
const TwitchChannel *twitchChannel)
{
MessageBuilder builder, builder2;
// Builder for low trust user message with explanation
builder.message().channelName = channelName;
builder.message().flags.set(MessageFlag::PubSub);
builder.message().flags.set(MessageFlag::LowTrustUsers);
// AutoMod shield badge
builder.emplace<BadgeElement>(makeAutoModBadge(),
MessageElementFlag::BadgeChannelAuthority);
// Suspicious user header message
QString prefix = "Suspicious User:";
builder.emplace<TextElement>(prefix, MessageElementFlag::Text,
MessageColor(QColor("blue")),
FontStyle::ChatMediumBold);
QString headerMessage;
if (action.treatment == PubSubLowTrustUsersMessage::Treatment::Restricted)
{
headerMessage = "Restricted";
builder2.message().flags.set(MessageFlag::RestrictedMessage);
}
else
{
headerMessage = "Monitored";
builder2.message().flags.set(MessageFlag::MonitoredMessage);
}
if (action.restrictionTypes.has(
PubSubLowTrustUsersMessage::RestrictionType::ManuallyAdded))
{
headerMessage += " by " + action.updatedByUserLogin;
}
headerMessage += " at " + action.updatedAt;
if (action.restrictionTypes.has(
PubSubLowTrustUsersMessage::RestrictionType::DetectedBanEvader))
{
QString evader;
if (action.evasionEvaluation ==
PubSubLowTrustUsersMessage::EvasionEvaluation::LikelyEvader)
{
evader = "likely";
}
else
{
evader = "possible";
}
headerMessage += ". Detected as " + evader + " ban evader";
}
if (action.restrictionTypes.has(
PubSubLowTrustUsersMessage::RestrictionType::BannedInSharedChannel))
{
headerMessage += ". Banned in " +
QString::number(action.sharedBanChannelIDs.size()) +
" shared channels";
}
builder.emplace<TextElement>(headerMessage, MessageElementFlag::Text,
MessageColor::Text);
builder.message().messageText = prefix + " " + headerMessage;
builder.message().searchText = prefix + " " + headerMessage;
auto message1 = builder.release();
//
// Builder for offender's message
builder2.message().channelName = channelName;
builder2
.emplace<TextElement>("#" + channelName,
MessageElementFlag::ChannelName,
MessageColor::System)
->setLink({Link::JumpToChannel, channelName});
builder2.emplace<TimestampElement>();
builder2.emplace<TwitchModerationElement>();
builder2.message().loginName = action.suspiciousUserLogin;
builder2.message().flags.set(MessageFlag::PubSub);
builder2.message().flags.set(MessageFlag::LowTrustUsers);
// sender badges
appendBadges(&builder2, action.senderBadges, {}, twitchChannel);
// sender username
builder2.emplace<MentionElement>(
action.suspiciousUserDisplayName + ":", action.suspiciousUserLogin,
MessageColor::Text, action.suspiciousUserColor);
// sender's message caught by AutoMod
for (const auto &fragment : action.fragments)
{
if (fragment.emoteID.isEmpty())
{
builder2.emplace<TextElement>(
fragment.text, MessageElementFlag::Text, MessageColor::Text);
}
else
{
const auto emotePtr =
getApp()->getEmotes()->getTwitchEmotes()->getOrCreateEmote(
EmoteId{fragment.emoteID}, EmoteName{fragment.text});
builder2.emplace<EmoteElement>(
emotePtr, MessageElementFlag::TwitchEmote, MessageColor::Text);
}
}
auto text =
QString("%1: %2").arg(action.suspiciousUserDisplayName, action.text);
builder2.message().messageText = text;
builder2.message().searchText = text;
auto message2 = builder2.release();
return std::make_pair(message1, message2);
}
MessagePtr MessageBuilder::makeLowTrustUpdateMessage(
const PubSubLowTrustUsersMessage &action)
{
/**
* Known issues:
* - Non-Twitch badges are not shown
* - Non-Twitch emotes are not shown
*/
MessageBuilder builder;
builder.emplace<TimestampElement>();
builder.message().flags.set(MessageFlag::System);
builder.message().flags.set(MessageFlag::PubSub);
builder.message().flags.set(MessageFlag::DoNotTriggerNotification);
builder
.emplace<TextElement>(action.updatedByUserDisplayName,
MessageElementFlag::Username,
MessageColor::System, FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, action.updatedByUserLogin});
QString text;
assert(action.treatment != PubSubLowTrustUsersMessage::Treatment::INVALID);
switch (action.treatment)
{
case PubSubLowTrustUsersMessage::Treatment::NoTreatment: {
builder.emplace<TextElement>("removed", MessageElementFlag::Text,
MessageColor::System);
builder
.emplace<TextElement>(action.suspiciousUserDisplayName,
MessageElementFlag::Username,
MessageColor::System,
FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, action.suspiciousUserLogin});
builder.emplace<TextElement>("from the suspicious user list.",
MessageElementFlag::Text,
MessageColor::System);
text = QString("%1 removed %2 from the suspicious user list.")
.arg(action.updatedByUserDisplayName,
action.suspiciousUserDisplayName);
}
break;
case PubSubLowTrustUsersMessage::Treatment::ActiveMonitoring: {
builder.emplace<TextElement>("added", MessageElementFlag::Text,
MessageColor::System);
builder
.emplace<TextElement>(action.suspiciousUserDisplayName,
MessageElementFlag::Username,
MessageColor::System,
FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, action.suspiciousUserLogin});
builder.emplace<TextElement>("as a monitored suspicious chatter.",
MessageElementFlag::Text,
MessageColor::System);
text = QString("%1 added %2 as a monitored suspicious chatter.")
.arg(action.updatedByUserDisplayName,
action.suspiciousUserDisplayName);
}
break;
case PubSubLowTrustUsersMessage::Treatment::Restricted: {
builder.emplace<TextElement>("added", MessageElementFlag::Text,
MessageColor::System);
builder
.emplace<TextElement>(action.suspiciousUserDisplayName,
MessageElementFlag::Username,
MessageColor::System,
FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, action.suspiciousUserLogin});
builder.emplace<TextElement>("as a restricted suspicious chatter.",
MessageElementFlag::Text,
MessageColor::System);
text = QString("%1 added %2 as a restricted suspicious chatter.")
.arg(action.updatedByUserDisplayName,
action.suspiciousUserDisplayName);
}
break;
default:
qCDebug(chatterinoTwitch) << "Unexpected suspicious treatment: "
<< action.treatmentString;
break;
}
builder->messageText = text;
builder->searchText = text;
return builder.release();
}
MessagePtrMut MessageBuilder::makeMissingScopesMessage(
const QString &missingScopes)
{
+1 -29
View File
@@ -4,29 +4,20 @@
#include "common/Outcome.hpp"
#include "messages/MessageColor.hpp"
#include "messages/MessageFlag.hpp"
#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp"
#include <IrcMessage>
#include <QRegularExpression>
#include <QString>
#include <QTime>
#include <QUrl>
#include <QVariant>
#include <ctime>
#include <memory>
#include <unordered_map>
#include <utility>
namespace chatterino {
struct BanAction;
struct UnbanAction;
struct WarnAction;
struct RaidAction;
struct UnraidAction;
struct AutomodAction;
struct AutomodUserAction;
struct AutomodInfoAction;
struct Message;
using MessagePtr = std::shared_ptr<const Message>;
using MessagePtrMut = std::shared_ptr<Message>;
@@ -43,7 +34,6 @@ class IgnorePhrase;
struct HelixVip;
using HelixModerator = HelixVip;
struct ChannelPointReward;
struct DeleteAction;
struct TwitchEmoteOccurrence;
namespace linkparser {
@@ -113,13 +103,6 @@ public:
MessageBuilder(TimeoutMessageTag, const QString &username,
const QString &durationInSeconds, bool multipleTimes,
const QDateTime &time);
MessageBuilder(const BanAction &action, const QDateTime &time,
uint32_t count = 1);
MessageBuilder(const UnbanAction &action, const QDateTime &time);
MessageBuilder(const WarnAction &action);
MessageBuilder(const RaidAction &action);
MessageBuilder(const UnraidAction &action);
MessageBuilder(const AutomodUserAction &action);
MessageBuilder(LiveUpdatesAddEmoteMessageTag, const QString &platform,
const QString &actor,
@@ -201,7 +184,6 @@ public:
bool hostOn);
static MessagePtr makeDeletionMessageFromIRC(
const MessagePtr &originalMessage);
static MessagePtr makeDeletionMessageFromPubSub(const DeleteAction &action);
static MessagePtr makeListOfUsersMessage(QString prefix, QStringList users,
Channel *channel,
MessageFlags extraFlags = {});
@@ -211,16 +193,6 @@ public:
static MessagePtr buildHypeChatMessage(Communi::IrcPrivateMessage *message);
static std::pair<MessagePtr, MessagePtr> makeAutomodMessage(
const AutomodAction &action, const QString &channelName);
static MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action);
static std::pair<MessagePtr, MessagePtr> makeLowTrustUserMessage(
const PubSubLowTrustUsersMessage &action, const QString &channelName,
const TwitchChannel *twitchChannel);
static MessagePtr makeLowTrustUpdateMessage(
const PubSubLowTrustUsersMessage &action);
/// @brief Builds a message out of an `ircMessage`.
///
/// Building a message won't cause highlights to be triggered. They will