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
+1
View File
@@ -24,6 +24,7 @@
- Dev: Conan will no longer generate a `CMakeUserPresets.json` file. (#6117)
- Dev: Pass `--force-openssl` when installing from CMake in Qt 6.8+. (#6129)
- Dev: Fixed `<build-tool> clean` not working correctly with generated sources. (#6154)
- Dev: Removed authenticated PubSub implementation. (#6158)
- Dev: Save settings in `aboutToQuit`. (#6159)
## 2.5.3
-1
View File
@@ -39,7 +39,6 @@
#include "providers/seventv/SeventvBadges.hpp"
#include "providers/seventv/SeventvEventAPI.hpp"
#include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/PubSubManager.hpp"
#include "providers/twitch/PubSubMessages.hpp"
#include "providers/twitch/TwitchChannel.hpp"
-8
View File
@@ -404,8 +404,6 @@ set(SOURCE_FILES
providers/twitch/ChannelPointReward.hpp
providers/twitch/IrcMessageHandler.cpp
providers/twitch/IrcMessageHandler.hpp
providers/twitch/PubSubActions.cpp
providers/twitch/PubSubActions.hpp
providers/twitch/PubSubClient.cpp
providers/twitch/PubSubClient.hpp
providers/twitch/PubSubClientOptions.hpp
@@ -452,18 +450,12 @@ set(SOURCE_FILES
providers/twitch/eventsub/SubscriptionRequest.cpp
providers/twitch/eventsub/SubscriptionRequest.hpp
providers/twitch/pubsubmessages/AutoMod.cpp
providers/twitch/pubsubmessages/AutoMod.hpp
providers/twitch/pubsubmessages/Base.cpp
providers/twitch/pubsubmessages/Base.hpp
providers/twitch/pubsubmessages/ChannelPoints.cpp
providers/twitch/pubsubmessages/ChannelPoints.hpp
providers/twitch/pubsubmessages/ChatModeratorAction.cpp
providers/twitch/pubsubmessages/ChatModeratorAction.hpp
providers/twitch/pubsubmessages/Listen.cpp
providers/twitch/pubsubmessages/Listen.hpp
providers/twitch/pubsubmessages/LowTrustUsers.cpp
providers/twitch/pubsubmessages/LowTrustUsers.hpp
providers/twitch/pubsubmessages/Message.hpp
providers/twitch/pubsubmessages/Unlisten.cpp
providers/twitch/pubsubmessages/Unlisten.hpp
@@ -10,7 +10,6 @@
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
#include "messages/MessageElement.hpp"
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Theme.hpp"
@@ -149,27 +148,7 @@ QString debugTest(const CommandContext &ctx)
const auto command = ctx.words.value(1);
if (command == "timeout-pubsub")
{
QJsonObject data;
data["created_by_user_id"] = ctx.twitchChannel->roomId();
data["created_by"] = ctx.twitchChannel->getName();
BanAction action(data, ctx.twitchChannel->roomId());
action.source.id = ctx.twitchChannel->roomId();
action.source.login = ctx.twitchChannel->getName();
action.target.id = "11148817";
action.target.login = "pajlada";
action.duration = 10;
MessageBuilder msg(action, QDateTime::currentDateTime());
msg->flags.set(MessageFlag::PubSub);
ctx.channel->addOrReplaceTimeout(msg.release(),
QDateTime::currentDateTime());
}
else if (command == "timeout-irc")
if (command == "timeout-irc")
{
auto nowMillis = QDateTime::currentDateTime().toSecsSinceEpoch();
-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
-13
View File
@@ -1,13 +0,0 @@
#include "providers/twitch/PubSubActions.hpp"
namespace chatterino {
PubSubAction::PubSubAction(const QJsonObject &data, const QString &_roomID)
: timestamp(std::chrono::steady_clock::now())
, roomID(_roomID)
{
this->source.id = data.value("created_by_user_id").toString();
this->source.login = data.value("created_by").toString();
}
} // namespace chatterino
-196
View File
@@ -1,196 +0,0 @@
#pragma once
#include "providers/twitch/pubsubmessages/AutoMod.hpp"
#include <QColor>
#include <QDebug>
#include <QJsonObject>
#include <QString>
#include <QStringList>
#include <chrono>
#include <cinttypes>
namespace chatterino {
struct ActionUser {
QString id;
QString login;
// displayName should be in format "login(localizedName)" for non-ascii usernames
QString displayName;
QColor color;
inline bool operator==(const ActionUser &rhs) const
{
return this->id == rhs.id && this->login == rhs.login &&
this->displayName == rhs.displayName && this->color == rhs.color;
}
};
inline QDebug operator<<(QDebug dbg, const ActionUser &user)
{
dbg.nospace() << "ActionUser(" << user.id << ", " << user.login << ", "
<< user.displayName << ", " << user.color << ")";
return dbg.maybeSpace();
}
struct PubSubAction {
PubSubAction() = default;
PubSubAction(const QJsonObject &data, const QString &_roomID);
ActionUser source;
std::chrono::steady_clock::time_point timestamp;
QString roomID;
};
// Used when a chat mode (i.e. slowmode, subscribers only mode) is enabled or
// disabled
struct ModeChangedAction : PubSubAction {
using PubSubAction::PubSubAction;
enum Mode {
Unknown,
Slow,
R9K,
SubscribersOnly,
EmoteOnly,
} mode;
// Whether the mode was turned on or off
enum State {
Off,
On,
} state;
uint32_t duration = 0;
const char *getModeName() const
{
switch (this->mode)
{
case Mode::Slow:
return "slow";
case Mode::R9K:
return "r9k";
case Mode::SubscribersOnly:
return "subscribers-only";
case Mode::EmoteOnly:
return "emote-only";
default:
return "unknown";
}
}
};
struct BanAction : PubSubAction {
using PubSubAction::PubSubAction;
ActionUser target;
QString reason;
uint32_t duration = 0;
bool isBan() const
{
return this->duration == 0;
}
};
struct DeleteAction : PubSubAction {
using PubSubAction::PubSubAction;
ActionUser target;
QString messageId;
QString messageText;
};
struct UnbanAction : PubSubAction {
using PubSubAction::PubSubAction;
ActionUser target;
enum {
Banned,
TimedOut,
} previousState;
bool wasBan() const
{
return this->previousState == Banned;
}
};
struct ClearChatAction : PubSubAction {
using PubSubAction::PubSubAction;
};
struct ModerationStateAction : PubSubAction {
using PubSubAction::PubSubAction;
ActionUser target;
// true = modded
// false = unmodded
bool modded;
};
struct AutomodAction : PubSubAction {
using PubSubAction::PubSubAction;
ActionUser target;
QString message;
QString reason;
PubSubAutoModQueueMessage::Reason reasonCode;
QString msgID;
};
struct AutomodUserAction : PubSubAction {
using PubSubAction::PubSubAction;
ActionUser target;
enum {
AddPermitted,
RemovePermitted,
AddBlocked,
RemoveBlocked,
Properties,
} type;
QString message;
};
struct AutomodInfoAction : PubSubAction {
using PubSubAction::PubSubAction;
enum {
OnHold,
Denied,
Approved,
} type;
};
struct RaidAction : PubSubAction {
using PubSubAction::PubSubAction;
QString target;
};
struct UnraidAction : PubSubAction {
using PubSubAction::PubSubAction;
};
struct WarnAction : PubSubAction {
using PubSubAction::PubSubAction;
ActionUser target;
QStringList reasons;
};
} // namespace chatterino
-7
View File
@@ -1,16 +1,9 @@
#include "providers/twitch/PubSubClient.hpp"
#include "common/QLogging.hpp"
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/PubSubHelpers.hpp"
#include "providers/twitch/PubSubMessages.hpp"
#include "singletons/Settings.hpp"
#include "util/DebugCount.hpp"
#include "util/Helpers.hpp"
#include "util/RapidjsonHelpers.hpp"
#include <exception>
#include <thread>
namespace chatterino {
+2 -691
View File
@@ -2,17 +2,11 @@
#include "Application.hpp"
#include "common/QLogging.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "providers/NetworkConfigurationProvider.hpp"
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/PubSubClient.hpp"
#include "providers/twitch/PubSubHelpers.hpp"
#include "providers/twitch/PubSubMessages.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "singletons/Settings.hpp"
#include "util/DebugCount.hpp"
#include "util/Helpers.hpp"
#include "util/RapidjsonHelpers.hpp"
#include "util/RenameThread.hpp"
#include <QJsonArray>
@@ -20,7 +14,6 @@
#include <algorithm>
#include <exception>
#include <iostream>
#include <memory>
#include <thread>
@@ -37,449 +30,6 @@ PubSub::PubSub(const QString &host, std::chrono::seconds pingInterval)
pingInterval,
})
{
this->moderationActionHandlers["clear"] = [this](const auto &data,
const auto &roomID) {
ClearChatAction action(data, roomID);
this->moderation.chatCleared.invoke(action);
};
this->moderationActionHandlers["slowoff"] = [this](const auto &data,
const auto &roomID) {
ModeChangedAction action(data, roomID);
action.mode = ModeChangedAction::Mode::Slow;
action.state = ModeChangedAction::State::Off;
this->moderation.modeChanged.invoke(action);
};
this->moderationActionHandlers["slow"] = [this](const auto &data,
const auto &roomID) {
ModeChangedAction action(data, roomID);
action.mode = ModeChangedAction::Mode::Slow;
action.state = ModeChangedAction::State::On;
const auto args = data.value("args").toArray();
if (args.empty())
{
qCDebug(chatterinoPubSub)
<< "Missing duration argument in slowmode on";
return;
}
bool ok;
action.duration = args.at(0).toString().toUInt(&ok, 10);
this->moderation.modeChanged.invoke(action);
};
this->moderationActionHandlers["r9kbetaoff"] = [this](const auto &data,
const auto &roomID) {
ModeChangedAction action(data, roomID);
action.mode = ModeChangedAction::Mode::R9K;
action.state = ModeChangedAction::State::Off;
this->moderation.modeChanged.invoke(action);
};
this->moderationActionHandlers["r9kbeta"] = [this](const auto &data,
const auto &roomID) {
ModeChangedAction action(data, roomID);
action.mode = ModeChangedAction::Mode::R9K;
action.state = ModeChangedAction::State::On;
this->moderation.modeChanged.invoke(action);
};
this->moderationActionHandlers["subscribersoff"] =
[this](const auto &data, const auto &roomID) {
ModeChangedAction action(data, roomID);
action.mode = ModeChangedAction::Mode::SubscribersOnly;
action.state = ModeChangedAction::State::Off;
this->moderation.modeChanged.invoke(action);
};
this->moderationActionHandlers["subscribers"] = [this](const auto &data,
const auto &roomID) {
ModeChangedAction action(data, roomID);
action.mode = ModeChangedAction::Mode::SubscribersOnly;
action.state = ModeChangedAction::State::On;
this->moderation.modeChanged.invoke(action);
};
this->moderationActionHandlers["emoteonlyoff"] =
[this](const auto &data, const auto &roomID) {
ModeChangedAction action(data, roomID);
action.mode = ModeChangedAction::Mode::EmoteOnly;
action.state = ModeChangedAction::State::Off;
this->moderation.modeChanged.invoke(action);
};
this->moderationActionHandlers["emoteonly"] = [this](const auto &data,
const auto &roomID) {
ModeChangedAction action(data, roomID);
action.mode = ModeChangedAction::Mode::EmoteOnly;
action.state = ModeChangedAction::State::On;
this->moderation.modeChanged.invoke(action);
};
this->moderationActionHandlers["unmod"] = [this](const auto &data,
const auto &roomID) {
ModerationStateAction action(data, roomID);
action.target.id = data.value("target_user_id").toString();
const auto args = data.value("args").toArray();
if (args.isEmpty())
{
return;
}
action.target.login = args[0].toString();
action.modded = false;
this->moderation.moderationStateChanged.invoke(action);
};
this->moderationActionHandlers["mod"] = [this](const auto &data,
const auto &roomID) {
ModerationStateAction action(data, roomID);
action.modded = true;
auto innerType = data.value("type").toString();
if (innerType == "chat_login_moderation")
{
// Don't display the old message type
return;
}
action.target.id = data.value("target_user_id").toString();
action.target.login = data.value("target_user_login").toString();
this->moderation.moderationStateChanged.invoke(action);
};
this->moderationActionHandlers["timeout"] = [this](const auto &data,
const auto &roomID) {
BanAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
action.target.id = data.value("target_user_id").toString();
const auto args = data.value("args").toArray();
if (args.size() < 2)
{
return;
}
action.target.login = args[0].toString();
bool ok;
action.duration = args[1].toString().toUInt(&ok, 10);
action.reason = args[2].toString(); // May be omitted
this->moderation.userBanned.invoke(action);
};
this->moderationActionHandlers["delete"] = [this](const auto &data,
const auto &roomID) {
DeleteAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
action.target.id = data.value("target_user_id").toString();
const auto args = data.value("args").toArray();
if (args.size() < 3)
{
return;
}
action.target.login = args[0].toString();
action.messageText = args[1].toString();
action.messageId = args[2].toString();
this->moderation.messageDeleted.invoke(action);
};
this->moderationActionHandlers["ban"] = [this](const auto &data,
const auto &roomID) {
BanAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
action.target.id = data.value("target_user_id").toString();
const auto args = data.value("args").toArray();
if (args.isEmpty())
{
return;
}
action.target.login = args[0].toString();
action.reason = args[1].toString(); // May be omitted
this->moderation.userBanned.invoke(action);
};
this->moderationActionHandlers["unban"] = [this](const auto &data,
const auto &roomID) {
UnbanAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
action.target.id = data.value("target_user_id").toString();
action.previousState = UnbanAction::Banned;
const auto args = data.value("args").toArray();
if (args.isEmpty())
{
return;
}
action.target.login = args[0].toString();
this->moderation.userUnbanned.invoke(action);
};
this->moderationActionHandlers["untimeout"] = [this](const auto &data,
const auto &roomID) {
UnbanAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
action.target.id = data.value("target_user_id").toString();
action.previousState = UnbanAction::TimedOut;
const auto args = data.value("args").toArray();
if (args.isEmpty())
{
return;
}
action.target.login = args[0].toString();
this->moderation.userUnbanned.invoke(action);
};
this->moderationActionHandlers["warn"] = [this](const auto &data,
const auto &roomID) {
WarnAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login =
data.value("created_by").toString(); // currently always empty
action.target.id = data.value("target_user_id").toString();
action.target.login = data.value("target_user_login").toString();
const auto reasons = data.value("args").toArray();
bool firstArg = true;
for (const auto &reasonValue : reasons)
{
if (firstArg)
{
// Skip first arg in the reasons array since it's not a reason
firstArg = false;
continue;
}
const auto &reason = reasonValue.toString();
if (!reason.isEmpty())
{
action.reasons.append(reason);
}
}
this->moderation.userWarned.invoke(action);
};
this->moderationActionHandlers["raid"] = [this](const auto &data,
const auto &roomID) {
RaidAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
const auto args = data.value("args").toArray();
if (args.isEmpty())
{
return;
}
action.target = args[0].toString();
this->moderation.raidStarted.invoke(action);
};
this->moderationActionHandlers["unraid"] = [this](const auto &data,
const auto &roomID) {
UnraidAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
this->moderation.raidCanceled.invoke(action);
};
this->moderationActionHandlers["automod_message_rejected"] =
[this](const auto &data, const auto &roomID) {
AutomodInfoAction action(data, roomID);
action.type = AutomodInfoAction::OnHold;
this->moderation.automodInfoMessage.invoke(action);
};
this->moderationActionHandlers["automod_message_denied"] =
[this](const auto &data, const auto &roomID) {
AutomodInfoAction action(data, roomID);
action.type = AutomodInfoAction::Denied;
this->moderation.automodInfoMessage.invoke(action);
};
this->moderationActionHandlers["automod_message_approved"] =
[this](const auto &data, const auto &roomID) {
AutomodInfoAction action(data, roomID);
action.type = AutomodInfoAction::Approved;
this->moderation.automodInfoMessage.invoke(action);
};
this->channelTermsActionHandlers["add_permitted_term"] =
[this](const auto &data, const auto &roomID) {
// This term got a pass through automod
AutomodUserAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
action.type = AutomodUserAction::AddPermitted;
action.message = data.value("text").toString();
action.source.login = data.value("requester_login").toString();
this->moderation.automodUserMessage.invoke(action);
};
this->channelTermsActionHandlers["add_blocked_term"] =
[this](const auto &data, const auto &roomID) {
// A term has been added
AutomodUserAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
action.type = AutomodUserAction::AddBlocked;
action.message = data.value("text").toString();
action.source.login = data.value("requester_login").toString();
this->moderation.automodUserMessage.invoke(action);
};
this->moderationActionHandlers["delete_permitted_term"] =
[this](const auto &data, const auto &roomID) {
// This term got deleted
AutomodUserAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
const auto args = data.value("args").toArray();
action.type = AutomodUserAction::RemovePermitted;
if (args.isEmpty())
{
return;
}
action.message = args[0].toString();
this->moderation.automodUserMessage.invoke(action);
};
this->channelTermsActionHandlers["delete_permitted_term"] =
[this](const auto &data, const auto &roomID) {
// This term got deleted
AutomodUserAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
action.type = AutomodUserAction::RemovePermitted;
action.message = data.value("text").toString();
action.source.login = data.value("requester_login").toString();
this->moderation.automodUserMessage.invoke(action);
};
this->moderationActionHandlers["delete_blocked_term"] =
[this](const auto &data, const auto &roomID) {
// This term got deleted
AutomodUserAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
const auto args = data.value("args").toArray();
action.type = AutomodUserAction::RemoveBlocked;
if (args.isEmpty())
{
return;
}
action.message = args[0].toString();
this->moderation.automodUserMessage.invoke(action);
};
this->channelTermsActionHandlers["delete_blocked_term"] =
[this](const auto &data, const auto &roomID) {
// This term got deleted
AutomodUserAction action(data, roomID);
action.source.id = data.value("created_by_user_id").toString();
action.source.login = data.value("created_by").toString();
action.type = AutomodUserAction::RemoveBlocked;
action.message = data.value("text").toString();
action.source.login = data.value("requester_login").toString();
this->moderation.automodUserMessage.invoke(action);
};
this->moderationActionHandlers["denied_automod_message"] =
[](const auto &data, const auto &roomID) {
// This message got denied by a moderator
// qCDebug(chatterinoPubSub) << rj::stringify(data);
};
this->moderationActionHandlers["approved_automod_message"] =
[](const auto &data, const auto &roomID) {
// This message got approved by a moderator
// qCDebug(chatterinoPubSub) << rj::stringify(data);
};
this->websocketClient.set_access_channels(websocketpp::log::alevel::all);
this->websocketClient.clear_access_channels(
websocketpp::log::alevel::frame_payload |
@@ -509,24 +59,6 @@ PubSub::~PubSub()
void PubSub::initialize()
{
this->start();
this->setAccount(getApp()->getAccounts()->twitch.getCurrent());
getApp()->getAccounts()->twitch.currentUserChanged.connect(
[this] {
this->unlistenChannelModerationActions();
this->unlistenAutomod();
this->unlistenLowTrustUsers();
this->unlistenChannelPointRewards();
this->setAccount(getApp()->getAccounts()->twitch.getCurrent());
},
boost::signals2::at_front);
}
void PubSub::setAccount(std::shared_ptr<TwitchAccount> account)
{
this->token_ = account->getOAuthToken();
this->userID_ = account->getUserId();
}
void PubSub::addClient()
@@ -617,108 +149,6 @@ void PubSub::stop()
this->thread->detach();
}
void PubSub::listenToChannelModerationActions(const QString &channelID)
{
if (getSettings()->enableExperimentalEventSub)
{
return;
}
if (this->userID_.isEmpty())
{
qCDebug(chatterinoPubSub) << "Unable to listen to moderation actions "
"topic, no user logged in";
return;
}
static const QString topicFormat("chat_moderator_actions.%1.%2");
assert(!channelID.isEmpty());
auto topic = topicFormat.arg(this->userID_, channelID);
if (this->isListeningToTopic(topic))
{
return;
}
qCDebug(chatterinoPubSub) << "Listen to topic" << topic;
this->listenToTopic(topic);
}
void PubSub::unlistenChannelModerationActions()
{
this->unlistenPrefix("chat_moderator_actions.");
}
void PubSub::listenToAutomod(const QString &channelID)
{
if (getSettings()->enableExperimentalEventSub)
{
return;
}
if (this->userID_.isEmpty())
{
qCDebug(chatterinoPubSub)
<< "Unable to listen to automod topic, no user logged in";
return;
}
static const QString topicFormat("automod-queue.%1.%2");
assert(!channelID.isEmpty());
auto topic = topicFormat.arg(this->userID_, channelID);
if (this->isListeningToTopic(topic))
{
return;
}
qCDebug(chatterinoPubSub) << "Listen to topic" << topic;
this->listenToTopic(topic);
}
void PubSub::unlistenAutomod()
{
this->unlistenPrefix("automod-queue.");
}
void PubSub::listenToLowTrustUsers(const QString &channelID)
{
if (getSettings()->enableExperimentalEventSub)
{
return;
}
if (this->userID_.isEmpty())
{
qCDebug(chatterinoPubSub)
<< "Unable to listen to low trust users topic, no user logged in";
return;
}
static const QString topicFormat("low-trust-users.%1.%2");
assert(!channelID.isEmpty());
auto topic = topicFormat.arg(this->userID_, channelID);
if (this->isListeningToTopic(topic))
{
return;
}
qCDebug(chatterinoPubSub) << "Listen to topic" << topic;
this->listenToTopic(topic);
}
void PubSub::unlistenLowTrustUsers()
{
this->unlistenPrefix("low-trust-users.");
}
void PubSub::listenToChannelPointRewards(const QString &channelID)
{
static const QString topicFormat("community-points-channel-v1.%1");
@@ -918,7 +348,6 @@ void PubSub::onConnectionOpen(WebsocketHandle hdl)
this->requests.begin() + topicsToTake);
PubSubListenMessage msg(newTopics);
msg.setToken(this->token_);
if (auto success = client->listen(msg); !success)
{
@@ -1100,70 +529,7 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message)
{
QString topic = message.topic;
if (topic.startsWith("chat_moderator_actions."))
{
auto oInnerMessage =
message.toInner<PubSubChatModeratorActionMessage>();
if (!oInnerMessage)
{
return;
}
auto innerMessage = *oInnerMessage;
auto topicParts = topic.split(".");
assert(topicParts.length() == 3);
// Channel ID where the moderator actions are coming from
auto channelID = topicParts[2];
switch (innerMessage.type)
{
case PubSubChatModeratorActionMessage::Type::ModerationAction: {
QString moderationAction =
innerMessage.data.value("moderation_action").toString();
auto handlerIt =
this->moderationActionHandlers.find(moderationAction);
if (handlerIt == this->moderationActionHandlers.end())
{
qCDebug(chatterinoPubSub)
<< "No handler found for moderation action"
<< moderationAction;
return;
}
// Invoke handler function
handlerIt->second(innerMessage.data, channelID);
}
break;
case PubSubChatModeratorActionMessage::Type::ChannelTermsAction: {
QString channelTermsAction =
innerMessage.data.value("type").toString();
auto handlerIt =
this->channelTermsActionHandlers.find(channelTermsAction);
if (handlerIt == this->channelTermsActionHandlers.end())
{
qCDebug(chatterinoPubSub)
<< "No handler found for channel terms action"
<< channelTermsAction;
return;
}
// Invoke handler function
handlerIt->second(innerMessage.data, channelID);
}
break;
case PubSubChatModeratorActionMessage::Type::INVALID:
default: {
qCDebug(chatterinoPubSub) << "Invalid moderator action type:"
<< innerMessage.typeString;
}
break;
}
}
else if (topic.startsWith("community-points-channel-v1."))
if (topic.startsWith("community-points-channel-v1."))
{
auto oInnerMessage =
message.toInner<PubSubCommunityPointsChannelV1Message>();
@@ -1193,55 +559,6 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message)
break;
}
}
else if (topic.startsWith("automod-queue."))
{
auto oInnerMessage = message.toInner<PubSubAutoModQueueMessage>();
if (!oInnerMessage)
{
return;
}
auto innerMessage = *oInnerMessage;
auto topicParts = topic.split(".");
assert(topicParts.length() == 3);
// Channel ID where the moderator actions are coming from
auto channelID = topicParts[2];
this->moderation.autoModMessageCaught.invoke(innerMessage, channelID);
}
else if (topic.startsWith("low-trust-users."))
{
auto oInnerMessage = message.toInner<PubSubLowTrustUsersMessage>();
if (!oInnerMessage)
{
return;
}
auto innerMessage = *oInnerMessage;
switch (innerMessage.type)
{
case PubSubLowTrustUsersMessage::Type::UserMessage: {
this->moderation.suspiciousMessageReceived.invoke(innerMessage);
}
break;
case PubSubLowTrustUsersMessage::Type::TreatmentUpdate: {
this->moderation.suspiciousTreatmentUpdated.invoke(
innerMessage);
}
break;
case PubSubLowTrustUsersMessage::Type::INVALID: {
qCWarning(chatterinoPubSub)
<< "Invalid low trust users event type:"
<< innerMessage.typeString;
}
break;
}
}
else
{
qCDebug(chatterinoPubSub) << "Unknown topic:" << topic;
@@ -1258,13 +575,7 @@ void PubSub::runThread()
void PubSub::listenToTopic(const QString &topic)
{
PubSubListenMessage msg({topic});
if (!topic.startsWith("community-points-channel-v1."))
{
msg.setToken(this->token_);
}
this->listen(std::move(msg));
this->listen(PubSubListenMessage({topic}));
}
} // namespace chatterino
-102
View File
@@ -18,7 +18,6 @@
#include <atomic>
#include <chrono>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <optional>
@@ -35,22 +34,6 @@ namespace chatterino {
class TwitchAccount;
class PubSubClient;
struct ClearChatAction;
struct DeleteAction;
struct ModeChangedAction;
struct ModerationStateAction;
struct BanAction;
struct UnbanAction;
struct PubSubAutoModQueueMessage;
struct AutomodAction;
struct AutomodUserAction;
struct AutomodInfoAction;
struct RaidAction;
struct UnraidAction;
struct WarnAction;
struct PubSubLowTrustUsersMessage;
struct PubSubWhisperMessage;
struct PubSubListenMessage;
struct PubSubMessage;
struct PubSubMessageMessage;
@@ -83,11 +66,6 @@ class PubSub
WebsocketClient websocketClient;
std::unique_ptr<std::thread> thread;
// Account credentials
// Set from setAccount
QString token_;
QString userID_;
public:
PubSub(const QString &host,
std::chrono::seconds pingInterval = std::chrono::seconds(15));
@@ -101,74 +79,10 @@ public:
/// Set up connections between itself & other parts of the application
void initialize();
struct {
Signal<ClearChatAction> chatCleared;
Signal<DeleteAction> messageDeleted;
Signal<ModeChangedAction> modeChanged;
Signal<ModerationStateAction> moderationStateChanged;
Signal<RaidAction> raidStarted;
Signal<UnraidAction> raidCanceled;
Signal<BanAction> userBanned;
Signal<UnbanAction> userUnbanned;
Signal<WarnAction> userWarned;
Signal<PubSubLowTrustUsersMessage> suspiciousMessageReceived;
Signal<PubSubLowTrustUsersMessage> suspiciousTreatmentUpdated;
// Message caught by automod
// channelID
pajlada::Signals::Signal<PubSubAutoModQueueMessage, QString>
autoModMessageCaught;
// Message blocked by moderator
Signal<AutomodAction> autoModMessageBlocked;
Signal<AutomodUserAction> automodUserMessage;
Signal<AutomodInfoAction> automodInfoMessage;
} moderation;
struct {
Signal<const QJsonObject &> redeemed;
} pointReward;
/**
* Listen to moderation actions in the given channel.
* This topic is relevant for everyone.
* For moderators, this topic includes blocked/permitted terms updates,
* roomstate changes, general mod/vip updates, all bans/timeouts/deletions.
* For normal users, this topic includes moderation actions that are targetted at the local user:
* automod catching a user's sent message, a moderator approving or denying their caught messages,
* the user gaining/losing mod/vip, the user receiving a ban/timeout/deletion.
*
* PubSub topic: chat_moderator_actions.{currentUserID}.{channelID}
*/
void listenToChannelModerationActions(const QString &channelID);
void unlistenChannelModerationActions();
/**
* Listen to Automod events in the given channel.
* This topic is only relevant for moderators.
* This will send events about incoming messages that
* are caught by Automod.
*
* PubSub topic: automod-queue.{currentUserID}.{channelID}
*/
void listenToAutomod(const QString &channelID);
void unlistenAutomod();
/**
* Listen to Low Trust events in the given channel.
* This topic is only relevant for moderators.
* This will fire events about suspicious treatment updates
* and messages sent by restricted/monitored users.
*
* PubSub topic: low-trust-users.{currentUserID}.{channelID}
*/
void listenToLowTrustUsers(const QString &channelID);
void unlistenLowTrustUsers();
/**
* Listen to incoming channel point redemptions in the given channel.
* This topic is relevant for everyone.
@@ -190,8 +104,6 @@ public:
} diag;
private:
void setAccount(std::shared_ptr<TwitchAccount> account);
void start();
void stop();
@@ -218,14 +130,6 @@ private:
std::owner_less<WebsocketHandle>>
clients;
std::unordered_map<
QString, std::function<void(const QJsonObject &, const QString &)>>
moderationActionHandlers;
std::unordered_map<
QString, std::function<void(const QJsonObject &, const QString &)>>
channelTermsActionHandlers;
void onMessage(websocketpp::connection_hdl hdl, WebsocketMessagePtr msg);
void onConnectionOpen(websocketpp::connection_hdl hdl);
void onConnectionFail(websocketpp::connection_hdl hdl);
@@ -266,12 +170,6 @@ private:
FRIEND_TEST(TwitchPubSubClient, DisconnectedAfter1s);
FRIEND_TEST(TwitchPubSubClient, ExceedTopicLimit);
FRIEND_TEST(TwitchPubSubClient, ExceedTopicLimitSingleStep);
FRIEND_TEST(TwitchPubSubClient, ReceivedWhisper);
FRIEND_TEST(TwitchPubSubClient, ModeratorActionsUserBanned);
FRIEND_TEST(TwitchPubSubClient, MissingToken);
FRIEND_TEST(TwitchPubSubClient, WrongToken);
FRIEND_TEST(TwitchPubSubClient, CorrectToken);
FRIEND_TEST(TwitchPubSubClient, AutoModMessageHeld);
#endif
};
+2 -5
View File
@@ -1,10 +1,7 @@
#pragma once
#include "providers/twitch/pubsubmessages/AutoMod.hpp" // IWYU pragma: export
#include "providers/twitch/pubsubmessages/Base.hpp" // IWYU pragma: export
#include "providers/twitch/pubsubmessages/Base.hpp" // IWYU pragma: export
#include "providers/twitch/pubsubmessages/ChannelPoints.hpp" // IWYU pragma: export
#include "providers/twitch/pubsubmessages/ChatModeratorAction.hpp" // IWYU pragma: export
#include "providers/twitch/pubsubmessages/Listen.hpp" // IWYU pragma: export
#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp" // IWYU pragma: export
#include "providers/twitch/pubsubmessages/Listen.hpp" // IWYU pragma: export
#include "providers/twitch/pubsubmessages/Message.hpp" // IWYU pragma: export
#include "providers/twitch/pubsubmessages/Unlisten.hpp" // IWYU pragma: export
-4
View File
@@ -1510,12 +1510,8 @@ void TwitchChannel::refreshPubSub()
auto currentAccount = getApp()->getAccounts()->twitch.getCurrent();
getApp()->getTwitchPubSub()->listenToChannelModerationActions(roomId);
if (this->hasModRights())
{
getApp()->getTwitchPubSub()->listenToAutomod(roomId);
getApp()->getTwitchPubSub()->listenToLowTrustUsers(roomId);
this->eventSubChannelModerateHandle =
getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{
.subscriptionType = "channel.moderate",
-515
View File
@@ -17,13 +17,10 @@
#include "providers/seventv/SeventvEventAPI.hpp"
#include "providers/twitch/api/Helix.hpp"
#include "providers/twitch/IrcMessageHandler.hpp"
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/PubSubManager.hpp"
#include "providers/twitch/pubsubmessages/AutoMod.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Settings.hpp"
#include "singletons/StreamerMode.hpp"
#include "singletons/WindowManager.hpp"
#include "util/PostToThread.hpp"
#include "util/RatelimitBucket.hpp"
@@ -240,518 +237,6 @@ void TwitchIrcServer::initialize()
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.chatCleared,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, actor{action.source.login}] {
auto now = QDateTime::currentDateTime();
chan->addOrReplaceClearChat(
MessageBuilder::makeClearChatMessage(now, actor), now);
if (getSettings()->hideModerated)
{
// XXX: This is expensive. We could use a layout request if the layout
// would store the previous message flags.
getApp()->getWindows()->forceLayoutChannelViews();
}
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.modeChanged,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
QString text =
QString("%1 turned %2 %3 mode.")
.arg(action.source.login)
.arg(action.state == ModeChangedAction::State::On ? "on"
: "off")
.arg(action.getModeName());
if (action.duration > 0)
{
text += QString(" (%1 seconds)").arg(action.duration);
}
postToThread([chan, text] {
chan->addSystemMessage(text);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.moderationStateChanged,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
QString text;
text = QString("%1 %2 %3.")
.arg(action.source.login,
(action.modded ? "modded" : "unmodded"),
action.target.login);
postToThread([chan, text] {
chan->addSystemMessage(text);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.userBanned,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
// TODO: Can we utilize some pubsub time field? maybe not worth
auto time = QDateTime::currentDateTime();
MessageBuilder msg(action, time);
msg->flags.set(MessageFlag::PubSub);
chan->addOrReplaceTimeout(msg.release(),
QDateTime::currentDateTime());
if (getSettings()->hideModerated)
{
// XXX: This is expensive. We could use a layout request if the layout
// would store the previous message flags.
getApp()->getWindows()->forceLayoutChannelViews();
}
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.userWarned,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
// TODO: Resolve the moderator's user ID into a full user here, so message can look better
postToThread([chan, action] {
MessageBuilder msg(action);
msg->flags.set(MessageFlag::PubSub);
chan->addMessage(msg.release(), MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.messageDeleted,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty() || getSettings()->hideDeletionActions)
{
return;
}
auto msg = MessageBuilder::makeDeletionMessageFromPubSub(action);
postToThread([chan, msg] {
auto replaced = false;
LimitedQueueSnapshot<MessagePtr> snapshot =
chan->getMessageSnapshot();
int snapshotLength = snapshot.size();
// without parens it doesn't build on windows
int end = (std::max)(0, snapshotLength - 200);
for (int i = snapshotLength - 1; i >= end; --i)
{
const auto &s = snapshot[i];
if (!s->flags.has(MessageFlag::PubSub) &&
s->timeoutUser == msg->timeoutUser)
{
chan->replaceMessage(s, msg);
replaced = true;
break;
}
}
if (!replaced)
{
chan->addMessage(msg, MessageContext::Original);
}
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.userUnbanned,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
// TODO: Can we utilize some pubsub time field? maybe not worth
auto time = QDateTime::currentDateTime();
auto msg = MessageBuilder(action, time).release();
postToThread([chan, msg] {
chan->addMessage(msg, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.suspiciousMessageReceived,
[this](const auto &action) {
if (action.treatment ==
PubSubLowTrustUsersMessage::Treatment::INVALID)
{
qCWarning(chatterinoTwitch)
<< "Received suspicious message with unknown "
"treatment:"
<< action.treatmentString;
return;
}
// monitored chats are received over irc; in the future, we will use pubsub instead
if (action.treatment !=
PubSubLowTrustUsersMessage::Treatment::Restricted)
{
return;
}
if (getApp()->getStreamerMode()->shouldHideModActions())
{
// NOTE: This completely stops the building of this action, rathern than only hiding it.
// If the user disabled streamer mode or the setting, there will be messages missing
return;
}
auto chan = this->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
auto twitchChannel = std::dynamic_pointer_cast<TwitchChannel>(chan);
if (!twitchChannel)
{
return;
}
postToThread([twitchChannel, action] {
const auto p = MessageBuilder::makeLowTrustUserMessage(
action, twitchChannel->getName(), twitchChannel.get());
twitchChannel->addMessage(p.first, MessageContext::Original);
twitchChannel->addMessage(p.second, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.suspiciousTreatmentUpdated,
[this](const auto &action) {
if (action.treatment ==
PubSubLowTrustUsersMessage::Treatment::INVALID)
{
qCWarning(chatterinoTwitch)
<< "Received suspicious user update with unknown "
"treatment:"
<< action.treatmentString;
return;
}
if (action.updatedByUserLogin.isEmpty())
{
return;
}
if (getApp()->getStreamerMode()->shouldHideModActions())
{
// NOTE: This completely stops the building of this action, rathern than only hiding it.
// If the user disabled streamer mode or the setting, there will be messages missing
return;
}
auto chan = this->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
auto msg = MessageBuilder::makeLowTrustUpdateMessage(action);
chan->addMessage(msg, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.autoModMessageCaught,
[this](const auto &msg, const QString &channelID) {
auto chan = this->getChannelOrEmptyByID(channelID);
if (chan->isEmpty())
{
return;
}
switch (msg.type)
{
case PubSubAutoModQueueMessage::Type::AutoModCaughtMessage: {
if (msg.status == "PENDING")
{
AutomodAction action(msg.data, channelID);
if (msg.reason ==
PubSubAutoModQueueMessage::Reason::BlockedTerm)
{
auto numBlockedTermsMatched =
msg.blockedTermsFound.size();
auto hideBlockedTerms =
getSettings()
->streamerModeHideBlockedTermText &&
getApp()->getStreamerMode()->isEnabled();
if (!msg.blockedTermsFound.empty())
{
if (hideBlockedTerms)
{
action.reason =
u"matches %1 blocked term%2"_s
.arg(numBlockedTermsMatched)
.arg(numBlockedTermsMatched > 1
? u"s"
: u"");
}
else
{
QStringList blockedTerms(
msg.blockedTermsFound.begin(),
msg.blockedTermsFound.end());
action.reason =
u"matches %1 blocked term%2 \"%3\""_s
.arg(numBlockedTermsMatched)
.arg(numBlockedTermsMatched > 1
? u"s"
: u"")
.arg(blockedTerms.join(u"\", \""));
}
}
else
{
action.reason = "blocked term usage";
}
}
else
{
action.reason = QString("%1 level %2")
.arg(msg.contentCategory)
.arg(msg.contentLevel);
}
action.msgID = msg.messageID;
action.message = msg.messageText;
action.reasonCode = msg.reason;
// this message also contains per-word automod data, which could be implemented
// extract sender data manually because Twitch loves not being consistent
QString senderDisplayName =
msg.senderUserDisplayName; // Might be transformed later
bool hasLocalizedName = false;
if (!msg.senderUserDisplayName.isEmpty())
{
// check for non-ascii display names
if (QString::compare(msg.senderUserDisplayName,
msg.senderUserLogin,
Qt::CaseInsensitive) != 0)
{
hasLocalizedName = true;
}
}
QColor senderColor = msg.senderUserChatColor;
QString senderColor_;
if (!senderColor.isValid() &&
getSettings()->colorizeNicknames)
{
// color may be not present if user is a grey-name
senderColor = getRandomColor(msg.senderUserID);
}
// handle username style based on prefered setting
switch (getSettings()->usernameDisplayMode.getValue())
{
case UsernameDisplayMode::Username: {
if (hasLocalizedName)
{
senderDisplayName = msg.senderUserLogin;
}
break;
}
case UsernameDisplayMode::LocalizedName: {
break;
}
case UsernameDisplayMode::
UsernameAndLocalizedName: {
if (hasLocalizedName)
{
senderDisplayName = QString("%1(%2)").arg(
msg.senderUserLogin,
msg.senderUserDisplayName);
}
break;
}
}
action.target =
ActionUser{msg.senderUserID, msg.senderUserLogin,
senderDisplayName, senderColor};
postToThread([chan, action] {
const auto p = MessageBuilder::makeAutomodMessage(
action, chan->getName());
chan->addMessage(p.first, MessageContext::Original);
chan->addMessage(p.second,
MessageContext::Original);
getApp()
->getTwitch()
->getAutomodChannel()
->addMessage(p.first, MessageContext::Original);
getApp()
->getTwitch()
->getAutomodChannel()
->addMessage(p.second,
MessageContext::Original);
if (getSettings()->showAutomodInMentions)
{
getApp()
->getTwitch()
->getMentionsChannel()
->addMessage(p.first,
MessageContext::Original);
getApp()
->getTwitch()
->getMentionsChannel()
->addMessage(p.second,
MessageContext::Original);
}
});
}
else
{
// Gray out approve/deny button upon "ALLOWED" and "DENIED" statuses
// They are versions of automod_message_(denied|approved) but for mods.
chan->disableMessage("automod_" + msg.messageID);
}
}
break;
case PubSubAutoModQueueMessage::Type::INVALID:
default: {
}
break;
}
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.autoModMessageBlocked,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p =
MessageBuilder::makeAutomodMessage(action, chan->getName());
chan->addMessage(p.first, MessageContext::Original);
chan->addMessage(p.second, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.automodUserMessage,
[this](const auto &action) {
if (getApp()->getStreamerMode()->shouldHideModActions())
{
// NOTE: This completely stops the building of this action, rathern than only hiding it.
// If the user disabled streamer mode or the setting, there will be messages missing
return;
}
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
auto msg = MessageBuilder(action).release();
postToThread([chan, msg] {
chan->addMessage(msg, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.automodInfoMessage,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p = MessageBuilder::makeAutomodInfoMessage(action);
chan->addMessage(p, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.raidStarted,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
auto msg = MessageBuilder(action).release();
postToThread([chan, msg] {
chan->addMessage(msg, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->moderation.raidCanceled,
[this](const auto &action) {
auto chan = this->getChannelOrEmptyByID(action.roomID);
if (chan->isEmpty())
{
return;
}
auto msg = MessageBuilder(action).release();
postToThread([chan, msg] {
chan->addMessage(msg, MessageContext::Original);
});
});
this->connections_.managedConnect(
getApp()->getTwitchPubSub()->pointReward.redeemed, [this](auto &data) {
QString channelId = data.value("channel_id").toString();
+1 -1
View File
@@ -9,7 +9,7 @@
#include "providers/twitch/eventsub/Controller.hpp"
#include "providers/twitch/eventsub/MessageBuilder.hpp"
#include "providers/twitch/eventsub/MessageHandlers.hpp"
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Settings.hpp"
@@ -3,8 +3,10 @@
#include "Application.hpp"
#include "common/Literals.hpp"
#include "messages/Emote.hpp"
#include "messages/Image.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
#include "messages/MessageElement.hpp"
#include "singletons/Resources.hpp"
#include "singletons/Settings.hpp"
#include "singletons/StreamerMode.hpp"
@@ -3,6 +3,7 @@
#include "Application.hpp"
#include "messages/Message.hpp"
#include "messages/MessageBuilder.hpp"
#include "messages/MessageElement.hpp"
#include "providers/twitch/eventsub/MessageBuilder.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "singletons/Settings.hpp"
@@ -1,69 +0,0 @@
#include "providers/twitch/pubsubmessages/AutoMod.hpp"
#include "util/QMagicEnum.hpp"
#include <QJsonArray>
namespace chatterino {
PubSubAutoModQueueMessage::PubSubAutoModQueueMessage(const QJsonObject &root)
: typeString(root.value("type").toString())
, data(root.value("data").toObject())
, status(this->data.value("status").toString())
{
auto oType = qmagicenum::enumCast<Type>(this->typeString);
if (oType.has_value())
{
this->type = oType.value();
}
this->reason =
qmagicenum::enumCast<Reason>(data.value("reason_code").toString())
.value_or(Reason::INVALID);
auto contentClassification =
data.value("content_classification").toObject();
this->contentCategory = contentClassification.value("category").toString();
this->contentLevel = contentClassification.value("level").toInt();
auto message = data.value("message").toObject();
this->messageID = message.value("id").toString();
auto messageContent = message.value("content").toObject();
this->messageText = messageContent.value("text").toString();
auto messageSender = message.value("sender").toObject();
this->senderUserID = messageSender.value("user_id").toString();
this->senderUserLogin = messageSender.value("login").toString();
this->senderUserDisplayName =
messageSender.value("display_name").toString();
this->senderUserChatColor =
QColor(messageSender.value("chat_color").toString());
if (this->reason == Reason::BlockedTerm)
{
// Attempt to read the blocked term(s) that caused this message to be blocked
const auto caughtMessageReason =
data.value("caught_message_reason").toObject();
const auto blockedTermFailure =
caughtMessageReason.value("blocked_term_failure").toObject();
const auto termsFound =
blockedTermFailure.value("terms_found").toArray();
for (const auto &termValue : termsFound)
{
const auto term = termValue.toObject();
const auto termText = term.value("text").toString();
if (!termText.isEmpty())
{
this->blockedTermsFound.insert(termText);
}
}
}
}
} // namespace chatterino
@@ -1,84 +0,0 @@
#pragma once
#include <magic_enum/magic_enum.hpp>
#include <QColor>
#include <QJsonObject>
#include <QString>
#include <QStringList>
#include <set>
namespace chatterino {
struct PubSubAutoModQueueMessage {
enum class Type {
AutoModCaughtMessage,
INVALID,
};
enum class Reason {
AutoMod,
BlockedTerm,
INVALID,
};
QString typeString;
Type type = Type::INVALID;
Reason reason = Reason::INVALID;
QJsonObject data;
QString status;
QString contentCategory;
int contentLevel{};
QString messageID;
QString messageText;
QString senderUserID;
QString senderUserLogin;
QString senderUserDisplayName;
QColor senderUserChatColor;
std::set<QString> blockedTermsFound;
PubSubAutoModQueueMessage() = default;
explicit PubSubAutoModQueueMessage(const QJsonObject &root);
};
} // namespace chatterino
template <>
constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
chatterino::PubSubAutoModQueueMessage::Type>(
chatterino::PubSubAutoModQueueMessage::Type value) noexcept
{
switch (value)
{
case chatterino::PubSubAutoModQueueMessage::Type::AutoModCaughtMessage:
return "automod_caught_message";
default:
return default_tag;
}
}
template <>
constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
chatterino::PubSubAutoModQueueMessage::Reason>(
chatterino::PubSubAutoModQueueMessage::Reason value) noexcept
{
switch (value)
{
case chatterino::PubSubAutoModQueueMessage::Reason::AutoMod:
return "AutoModCaughtMessageReason";
case chatterino::PubSubAutoModQueueMessage::Reason::BlockedTerm:
return "BlockedTermCaughtMessageReason";
default:
return default_tag;
}
}
@@ -1,19 +0,0 @@
#include "providers/twitch/pubsubmessages/ChatModeratorAction.hpp"
#include "util/QMagicEnum.hpp"
namespace chatterino {
PubSubChatModeratorActionMessage::PubSubChatModeratorActionMessage(
const QJsonObject &root)
: typeString(root.value("type").toString())
, data(root.value("data").toObject())
{
auto oType = qmagicenum::enumCast<Type>(this->typeString);
if (oType.has_value())
{
this->type = oType.value();
}
}
} // namespace chatterino
@@ -1,45 +0,0 @@
#pragma once
#include <magic_enum/magic_enum.hpp>
#include <QJsonObject>
#include <QString>
namespace chatterino {
struct PubSubChatModeratorActionMessage {
enum class Type {
ModerationAction,
ChannelTermsAction,
INVALID,
};
QString typeString;
Type type = Type::INVALID;
QJsonObject data;
PubSubChatModeratorActionMessage(const QJsonObject &root);
};
} // namespace chatterino
template <>
constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
chatterino::PubSubChatModeratorActionMessage::Type>(
chatterino::PubSubChatModeratorActionMessage::Type value) noexcept
{
switch (value)
{
case chatterino::PubSubChatModeratorActionMessage::Type::
ModerationAction:
return "moderation_action";
case chatterino::PubSubChatModeratorActionMessage::Type::
ChannelTermsAction:
return "channel_terms_action";
default:
return default_tag;
}
}
@@ -1,106 +0,0 @@
#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp"
#include "util/QMagicEnum.hpp"
#include <QDateTime>
#include <QJsonArray>
namespace chatterino {
PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root)
: typeString(root.value("type").toString())
{
if (const auto oType = qmagicenum::enumCast<Type>(this->typeString);
oType.has_value())
{
this->type = oType.value();
}
auto data = root.value("data").toObject();
if (this->type == Type::UserMessage)
{
this->msgID = data.value("message_id").toString();
this->sentAt = data.value("sent_at").toString();
const auto content = data.value("message_content").toObject();
this->text = content.value("text").toString();
for (const auto &part : content.value("fragments").toArray())
{
this->fragments.emplace_back(part.toObject());
}
// the rest of the data is within a nested object
data = data.value("low_trust_user").toObject();
const auto sender = data.value("sender").toObject();
this->suspiciousUserID = sender.value("user_id").toString();
this->suspiciousUserLogin = sender.value("login").toString();
this->suspiciousUserDisplayName =
sender.value("display_name").toString();
this->suspiciousUserColor =
QColor(sender.value("chat_color").toString());
for (const auto &badge : sender.value("badges").toArray())
{
const auto badgeObj = badge.toObject();
const auto badgeID = badgeObj.value("id").toString();
const auto badgeVersion = badgeObj.value("version").toString();
this->senderBadges.emplace_back(Badge{badgeID, badgeVersion});
}
const auto sharedValue = data.value("shared_ban_channel_ids");
if (!sharedValue.isNull())
{
for (const auto &id : sharedValue.toArray())
{
this->sharedBanChannelIDs.emplace_back(id.toString());
}
}
}
else
{
this->suspiciousUserID = data.value("target_user_id").toString();
this->suspiciousUserLogin = data.value("target_user").toString();
this->suspiciousUserDisplayName = this->suspiciousUserLogin;
}
this->channelID = data.value("channel_id").toString();
this->updatedAtString = data.value("updated_at").toString();
this->updatedAt = QDateTime::fromString(this->updatedAtString, Qt::ISODate)
.toLocalTime()
.toString("MMM d yyyy, h:mm ap");
const auto updatedBy = data.value("updated_by").toObject();
this->updatedByUserID = updatedBy.value("id").toString();
this->updatedByUserLogin = updatedBy.value("login").toString();
this->updatedByUserDisplayName = updatedBy.value("display_name").toString();
this->treatmentString = data.value("treatment").toString();
if (const auto oTreatment =
qmagicenum::enumCast<Treatment>(this->treatmentString);
oTreatment.has_value())
{
this->treatment = oTreatment.value();
}
this->evasionEvaluationString =
data.value("ban_evasion_evaluation").toString();
if (const auto oEvaluation = qmagicenum::enumCast<EvasionEvaluation>(
this->evasionEvaluationString);
oEvaluation.has_value())
{
this->evasionEvaluation = oEvaluation.value();
}
for (const auto &rType : data.value("types").toArray())
{
if (const auto oRestriction =
qmagicenum::enumCast<RestrictionType>(rType.toString());
oRestriction.has_value())
{
this->restrictionTypes.set(oRestriction.value());
}
}
}
} // namespace chatterino
@@ -1,266 +0,0 @@
#pragma once
#include "providers/twitch/TwitchBadge.hpp"
#include <common/FlagsEnum.hpp>
#include <magic_enum/magic_enum.hpp>
#include <QColor>
#include <QJsonObject>
#include <QString>
namespace chatterino {
struct PubSubLowTrustUsersMessage {
struct Fragment {
QString text;
QString emoteID;
explicit Fragment(const QJsonObject &obj)
: text(obj.value("text").toString())
, emoteID(obj.value("emoticon")
.toObject()
.value("emoticonID")
.toString())
{
}
};
/**
* The type of low trust message update
*/
enum class Type {
/**
* An incoming message from someone marked as low trust
*/
UserMessage,
/**
* An incoming update about a user's low trust status
*/
TreatmentUpdate,
INVALID,
};
/**
* The treatment set for the suspicious user
*/
enum class Treatment {
NoTreatment,
ActiveMonitoring,
Restricted,
INVALID,
};
/**
* A ban evasion likelihood value (if any) that has been applied to the user
* automatically by Twitch
*/
enum class EvasionEvaluation {
UnknownEvader,
UnlikelyEvader,
LikelyEvader,
PossibleEvader,
INVALID,
};
/**
* Restriction type (if any) that apply to the suspicious user
*/
enum class RestrictionType : uint8_t {
UnknownType = 1 << 0,
ManuallyAdded = 1 << 1,
DetectedBanEvader = 1 << 2,
BannedInSharedChannel = 1 << 3,
INVALID = 1 << 4,
};
Type type = Type::INVALID;
Treatment treatment = Treatment::INVALID;
EvasionEvaluation evasionEvaluation = EvasionEvaluation::INVALID;
FlagsEnum<RestrictionType> restrictionTypes;
QString channelID;
QString suspiciousUserID;
QString suspiciousUserLogin;
QString suspiciousUserDisplayName;
QString updatedByUserID;
QString updatedByUserLogin;
QString updatedByUserDisplayName;
/**
* Formatted timestamp of when the treatment was last updated for the suspicious user
*/
QString updatedAt;
/**
* Plain text of the message sent.
* Only used for the UserMessage type.
*/
QString text;
/**
* Pre-parsed components of the message.
* Only used for the UserMessage type.
*/
std::vector<Fragment> fragments;
/**
* ID of the message.
* Only used for the UserMessage type.
*/
QString msgID;
/**
* RFC3339 timestamp of when the message was sent.
* Only used for the UserMessage type.
*/
QString sentAt;
/**
* Color of the user who sent the message.
* Only used for the UserMessage type.
*/
QColor suspiciousUserColor;
/**
* A list of channel IDs where the suspicious user is also banned.
* Only used for the UserMessage type.
*/
std::vector<QString> sharedBanChannelIDs;
/**
* A list of badges of the user who sent the message.
* Only used for the UserMessage type.
*/
std::vector<Badge> senderBadges;
/**
* Stores the string value of `type`
* Useful in case type shows up as invalid after being parsed
*/
QString typeString;
/**
* Stores the string value of `treatment`
* Useful in case treatment shows up as invalid after being parsed
*/
QString treatmentString;
/**
* Stores the string value of `ban_evasion_evaluation`
* Useful in case evasionEvaluation shows up as invalid after being parsed
*/
QString evasionEvaluationString;
/**
* Stores the string value of `updated_at`
* Useful in case formattedUpdatedAt doesn't parse correctly
*/
QString updatedAtString;
PubSubLowTrustUsersMessage() = default;
explicit PubSubLowTrustUsersMessage(const QJsonObject &root);
};
} // namespace chatterino
template <>
constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
chatterino::PubSubLowTrustUsersMessage::Type>(
chatterino::PubSubLowTrustUsersMessage::Type value) noexcept
{
switch (value)
{
case chatterino::PubSubLowTrustUsersMessage::Type::UserMessage:
return "low_trust_user_new_message";
case chatterino::PubSubLowTrustUsersMessage::Type::TreatmentUpdate:
return "low_trust_user_treatment_update";
default:
return default_tag;
}
}
template <>
constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
chatterino::PubSubLowTrustUsersMessage::Treatment>(
chatterino::PubSubLowTrustUsersMessage::Treatment value) noexcept
{
using Treatment = chatterino::PubSubLowTrustUsersMessage::Treatment;
switch (value)
{
case Treatment::NoTreatment:
return "NO_TREATMENT";
case Treatment::ActiveMonitoring:
return "ACTIVE_MONITORING";
case Treatment::Restricted:
return "RESTRICTED";
default:
return default_tag;
}
}
template <>
constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
chatterino::PubSubLowTrustUsersMessage::EvasionEvaluation>(
chatterino::PubSubLowTrustUsersMessage::EvasionEvaluation value) noexcept
{
using EvasionEvaluation =
chatterino::PubSubLowTrustUsersMessage::EvasionEvaluation;
switch (value)
{
case EvasionEvaluation::UnknownEvader:
return "UNKNOWN_EVADER";
case EvasionEvaluation::UnlikelyEvader:
return "UNLIKELY_EVADER";
case EvasionEvaluation::LikelyEvader:
return "LIKELY_EVADER";
case EvasionEvaluation::PossibleEvader:
return "POSSIBLE_EVADER";
default:
return default_tag;
}
}
template <>
constexpr magic_enum::customize::customize_t magic_enum::customize::enum_name<
chatterino::PubSubLowTrustUsersMessage::RestrictionType>(
chatterino::PubSubLowTrustUsersMessage::RestrictionType value) noexcept
{
using RestrictionType =
chatterino::PubSubLowTrustUsersMessage::RestrictionType;
switch (value)
{
case RestrictionType::UnknownType:
return "UNKNOWN_TYPE";
case RestrictionType::ManuallyAdded:
return "MANUALLY_ADDED";
case RestrictionType::DetectedBanEvader:
return "DETECTED_BAN_EVADER";
case RestrictionType::BannedInSharedChannel:
return "BANNED_IN_SHARED_CHANNEL";
default:
return default_tag;
}
}
+9 -181
View File
@@ -1,8 +1,6 @@
#include "mocks/BaseApplication.hpp"
#include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/PubSubClient.hpp"
#include "providers/twitch/PubSubManager.hpp"
#include "providers/twitch/pubsubmessages/AutoMod.hpp"
#include "providers/twitch/TwitchAccount.hpp"
#include "Test.hpp"
@@ -20,11 +18,7 @@ using namespace std::chrono_literals;
* Server randomly disconnects us, we should reconnect (COMPLETE)
* Client listens to more than 50 topics, so it opens 2 connections (COMPLETE)
* Server sends RECONNECT message to us, we should reconnect (INCOMPLETE, leaving for now since if we just ignore it and Twitch disconnects us we should already handle it properly)
* Listen that required authentication, but authentication is missing (COMPLETE)
* Listen that required authentication, but authentication is wrong (COMPLETE)
* Incoming AutoMod message
* Incoming ChannelPoints message
* Incoming ChatModeratorAction message (COMPLETE)
**/
#define RUN_PUBSUB_TESTS
@@ -85,23 +79,18 @@ const QString TEST_SETTINGS = R"(
class FTest : public PubSub
{
public:
explicit FTest(const char *path, std::chrono::seconds pingInterval,
QString token = "token")
explicit FTest(const char *path, std::chrono::seconds pingInterval)
: PubSub(QString("wss://127.0.0.1:9050%1").arg(path), pingInterval)
{
auto account = std::make_shared<TwitchAccount>("testaccount_420", token,
"clientid", "123456");
this->setAccount(account);
}
};
class MockApplication : public mock::BaseApplication
{
public:
MockApplication(const char *path, std::chrono::seconds pingInterval,
QString token = "token")
MockApplication(const char *path, std::chrono::seconds pingInterval)
: mock::BaseApplication(TEST_SETTINGS)
, pubSub(path, pingInterval, token)
, pubSub(path, pingInterval)
{
}
@@ -127,7 +116,7 @@ TEST(TwitchPubSubClient, ServerRespondsToPings)
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
ASSERT_EQ(pubSub.diag.messagesReceived, 0);
pubSub.listenToChannelModerationActions("123456");
pubSub.listenToChannelPointRewards("123456");
std::this_thread::sleep_for(150ms);
@@ -159,7 +148,7 @@ TEST(TwitchPubSubClient, ServerDoesntRespondToPings)
auto &pubSub = a.pubSub;
pubSub.start();
pubSub.listenToChannelModerationActions("123456");
pubSub.listenToChannelPointRewards("123456");
std::this_thread::sleep_for(750ms);
@@ -198,7 +187,7 @@ TEST(TwitchPubSubClient, DisconnectedAfter1s)
ASSERT_EQ(pubSub.diag.messagesReceived, 0);
ASSERT_EQ(pubSub.diag.listenResponses, 0);
pubSub.listenToChannelModerationActions("123456");
pubSub.listenToChannelPointRewards("123456");
std::this_thread::sleep_for(500ms);
@@ -233,7 +222,7 @@ TEST(TwitchPubSubClient, ExceedTopicLimit)
for (auto i = 0; i < PubSubClient::MAX_LISTENS; ++i)
{
pubSub.listenToChannelModerationActions(QString("1%1").arg(i));
pubSub.listenToChannelPointRewards(QString("1%1").arg(i));
}
std::this_thread::sleep_for(50ms);
@@ -244,7 +233,7 @@ TEST(TwitchPubSubClient, ExceedTopicLimit)
for (auto i = 0; i < PubSubClient::MAX_LISTENS; ++i)
{
pubSub.listenToChannelModerationActions(QString("2%1").arg(i));
pubSub.listenToChannelPointRewards(QString("2%1").arg(i));
}
std::this_thread::sleep_for(50ms);
@@ -274,7 +263,7 @@ TEST(TwitchPubSubClient, ExceedTopicLimitSingleStep)
for (auto i = 0; i < PubSubClient::MAX_LISTENS * 2; ++i)
{
pubSub.listenToChannelModerationActions("123456");
pubSub.listenToChannelPointRewards("123456");
}
std::this_thread::sleep_for(150ms);
@@ -290,167 +279,6 @@ TEST(TwitchPubSubClient, ExceedTopicLimitSingleStep)
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
}
TEST(TwitchPubSubClient, ModeratorActionsUserBanned)
{
MockApplication a("/moderator-actions-user-banned", 1s);
auto &pubSub = a.pubSub;
pubSub.start();
ReceivedMessage<BanAction> received;
std::ignore =
pubSub.moderation.userBanned.connect([&received](const auto &action) {
received = action;
});
ASSERT_EQ(pubSub.diag.listenResponses, 0);
pubSub.listenToChannelModerationActions("123456");
std::this_thread::sleep_for(50ms);
ASSERT_EQ(pubSub.diag.connectionsOpened, 1);
ASSERT_EQ(pubSub.diag.connectionsClosed, 0);
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
ASSERT_EQ(pubSub.diag.messagesReceived, 3);
ASSERT_EQ(pubSub.diag.listenResponses, 1);
ASSERT_TRUE(received);
ActionUser expectedTarget{"140114344", "1xelerate", "", QColor()};
ActionUser expectedSource{"117691339", "mm2pl", "", QColor()};
ASSERT_EQ(received->reason, QString());
ASSERT_EQ(received->duration, 0);
ASSERT_EQ(received->target, expectedTarget);
ASSERT_EQ(received->source, expectedSource);
pubSub.stop();
ASSERT_EQ(pubSub.diag.connectionsOpened, 1);
ASSERT_EQ(pubSub.diag.connectionsClosed, 1);
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
}
TEST(TwitchPubSubClient, MissingToken)
{
// The token that's required is "xD"
MockApplication a("/authentication-required", 1s, "");
auto &pubSub = a.pubSub;
pubSub.start();
pubSub.listenToChannelModerationActions("123456");
std::this_thread::sleep_for(150ms);
ASSERT_EQ(pubSub.diag.connectionsOpened, 1);
ASSERT_EQ(pubSub.diag.connectionsClosed, 0);
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
ASSERT_EQ(pubSub.diag.messagesReceived, 2);
ASSERT_EQ(pubSub.diag.listenResponses, 0);
ASSERT_EQ(pubSub.diag.failedListenResponses, 1);
pubSub.stop();
ASSERT_EQ(pubSub.diag.connectionsOpened, 1);
ASSERT_EQ(pubSub.diag.connectionsClosed, 1);
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
}
TEST(TwitchPubSubClient, WrongToken)
{
// The token that's required is "xD"
MockApplication a("/authentication-required", 1s);
auto &pubSub = a.pubSub;
pubSub.start();
pubSub.listenToChannelModerationActions("123456");
std::this_thread::sleep_for(50ms);
ASSERT_EQ(pubSub.diag.connectionsOpened, 1);
ASSERT_EQ(pubSub.diag.connectionsClosed, 0);
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
ASSERT_EQ(pubSub.diag.messagesReceived, 2);
ASSERT_EQ(pubSub.diag.listenResponses, 0);
ASSERT_EQ(pubSub.diag.failedListenResponses, 1);
pubSub.stop();
ASSERT_EQ(pubSub.diag.connectionsOpened, 1);
ASSERT_EQ(pubSub.diag.connectionsClosed, 1);
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
}
TEST(TwitchPubSubClient, CorrectToken)
{
// The token that's required is "xD"
MockApplication a("/authentication-required", 1s, "xD");
auto &pubSub = a.pubSub;
pubSub.start();
pubSub.listenToChannelModerationActions("123456");
std::this_thread::sleep_for(50ms);
ASSERT_EQ(pubSub.diag.connectionsOpened, 1);
ASSERT_EQ(pubSub.diag.connectionsClosed, 0);
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
ASSERT_EQ(pubSub.diag.messagesReceived, 2);
ASSERT_EQ(pubSub.diag.listenResponses, 1);
ASSERT_EQ(pubSub.diag.failedListenResponses, 0);
pubSub.stop();
ASSERT_EQ(pubSub.diag.connectionsOpened, 1);
ASSERT_EQ(pubSub.diag.connectionsClosed, 1);
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
}
TEST(TwitchPubSubClient, AutoModMessageHeld)
{
MockApplication a("/automod-held", 1s);
auto &pubSub = a.pubSub;
pubSub.start();
ReceivedMessage<PubSubAutoModQueueMessage> received;
ReceivedMessage<QString> channelID;
std::ignore = pubSub.moderation.autoModMessageCaught.connect(
[&](const auto &msg, const QString &incomingChannelID) {
received = msg;
channelID = incomingChannelID;
});
pubSub.listenToAutomod("117166826");
std::this_thread::sleep_for(50ms);
ASSERT_EQ(pubSub.diag.connectionsOpened, 1);
ASSERT_EQ(pubSub.diag.connectionsClosed, 0);
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
ASSERT_EQ(pubSub.diag.messagesReceived, 3);
ASSERT_EQ(pubSub.diag.listenResponses, 1);
ASSERT_EQ(pubSub.diag.failedListenResponses, 0);
ASSERT_TRUE(received);
ASSERT_TRUE(channelID);
ASSERT_EQ(channelID, "117166826");
ASSERT_EQ(received->messageText, "kurwa");
pubSub.stop();
ASSERT_EQ(pubSub.diag.connectionsOpened, 1);
ASSERT_EQ(pubSub.diag.connectionsClosed, 1);
ASSERT_EQ(pubSub.diag.connectionsFailed, 0);
}
} // namespace chatterino
#endif