feat: show restricted chats and suspicious treatment updates (#5056)

Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
This commit is contained in:
iProdigy 2023-12-31 04:44:55 -06:00 committed by GitHub
parent 69a54d944d
commit 036a5f3f21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 769 additions and 11 deletions

View file

@ -4,6 +4,7 @@
- Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922) - Major: Allow use of Twitch follower emotes in other channels if subscribed. (#4922)
- Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026) - Major: Add `/automod` split to track automod caught messages across all open channels the user moderates. (#4986, #5026)
- Major: Show restricted chat messages and suspicious treatment updates. (#5056)
- Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809) - Minor: Migrate to the new Get Channel Followers Helix endpoint, fixing follower count not showing up in usercards. (#4809)
- Minor: The account switcher is now styled to match your theme. (#4817) - Minor: The account switcher is now styled to match your theme. (#4817)
- Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795) - Minor: Add an invisible resize handle to the bottom of frameless user info popups and reply thread popups. (#4795)
@ -59,6 +60,7 @@
- Bugfix: Fixes to section deletion in text input fields. (#5013) - Bugfix: Fixes to section deletion in text input fields. (#5013)
- Bugfix: Show user text input within watch streak notices. (#5029) - Bugfix: Show user text input within watch streak notices. (#5029)
- Bugfix: Fixed avatar in usercard and moderation button triggering when releasing the mouse outside their area. (#5052) - Bugfix: Fixed avatar in usercard and moderation button triggering when releasing the mouse outside their area. (#5052)
- Bugfix: Fixed moderator-only topics being subscribed to for non-moderators. (#5056)
- Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978) - Dev: Run miniaudio in a separate thread, and simplify it to not manage the device ourselves. There's a chance the simplification is a bad idea. (#4978)
- Dev: Change clang-format from v14 to v16. (#4929) - Dev: Change clang-format from v14 to v16. (#4929)
- Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791)

View file

@ -35,6 +35,7 @@
#include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubActions.hpp"
#include "providers/twitch/PubSubManager.hpp" #include "providers/twitch/PubSubManager.hpp"
#include "providers/twitch/PubSubMessages.hpp" #include "providers/twitch/PubSubMessages.hpp"
#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp"
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "providers/twitch/TwitchMessageBuilder.hpp" #include "providers/twitch/TwitchMessageBuilder.hpp"
@ -473,6 +474,87 @@ void Application::initPubSub()
}); });
}); });
std::ignore =
this->twitch->pubsub->signals_.moderation.suspiciousMessageReceived
.connect([&](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 (getSettings()->streamerModeHideModActions &&
isInStreamerMode())
{
return;
}
auto chan =
this->twitch->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
const auto p =
TwitchMessageBuilder::makeLowTrustUserMessage(
action, chan->getName());
chan->addMessage(p.first);
chan->addMessage(p.second);
});
});
std::ignore =
this->twitch->pubsub->signals_.moderation.suspiciousTreatmentUpdated
.connect([&](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 (getSettings()->streamerModeHideModActions &&
isInStreamerMode())
{
return;
}
auto chan =
this->twitch->getChannelOrEmptyByID(action.channelID);
if (chan->isEmpty())
{
return;
}
postToThread([chan, action] {
auto msg =
TwitchMessageBuilder::makeLowTrustUpdateMessage(action);
chan->addMessage(msg);
});
});
std::ignore = std::ignore =
this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect( this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect(
[&](const auto &msg, const QString &channelID) { [&](const auto &msg, const QString &channelID) {
@ -672,6 +754,7 @@ void Application::initPubSub()
[this] { [this] {
this->twitch->pubsub->unlistenAllModerationActions(); this->twitch->pubsub->unlistenAllModerationActions();
this->twitch->pubsub->unlistenAutomod(); this->twitch->pubsub->unlistenAutomod();
this->twitch->pubsub->unlistenLowTrustUsers();
this->twitch->pubsub->unlistenWhispers(); this->twitch->pubsub->unlistenWhispers();
}, },
boost::signals2::at_front); boost::signals2::at_front);

View file

@ -412,6 +412,8 @@ set(SOURCE_FILES
providers/twitch/pubsubmessages/ChatModeratorAction.hpp providers/twitch/pubsubmessages/ChatModeratorAction.hpp
providers/twitch/pubsubmessages/Listen.cpp providers/twitch/pubsubmessages/Listen.cpp
providers/twitch/pubsubmessages/Listen.hpp providers/twitch/pubsubmessages/Listen.hpp
providers/twitch/pubsubmessages/LowTrustUsers.cpp
providers/twitch/pubsubmessages/LowTrustUsers.hpp
providers/twitch/pubsubmessages/Message.hpp providers/twitch/pubsubmessages/Message.hpp
providers/twitch/pubsubmessages/Unlisten.cpp providers/twitch/pubsubmessages/Unlisten.cpp
providers/twitch/pubsubmessages/Unlisten.hpp providers/twitch/pubsubmessages/Unlisten.hpp

View file

@ -315,7 +315,6 @@ bool Channel::isBroadcaster() const
bool Channel::hasModRights() const bool Channel::hasModRights() const
{ {
// fourtf: check if staff
return this->isMod() || this->isBroadcaster(); return this->isMod() || this->isBroadcaster();
} }

View file

@ -52,6 +52,7 @@ enum class MessageFlag : int64_t {
LiveUpdatesUpdate = (1LL << 30), LiveUpdatesUpdate = (1LL << 30),
/// The message caught by AutoMod containing the user who sent the message & its contents /// The message caught by AutoMod containing the user who sent the message & its contents
AutoModOffendingMessage = (1LL << 31), AutoModOffendingMessage = (1LL << 31),
LowTrustUsers = (1LL << 32),
}; };
using MessageFlags = FlagsEnum<MessageFlag>; using MessageFlags = FlagsEnum<MessageFlag>;

View file

@ -367,7 +367,8 @@ void MessageLayout::updateBuffer(QPixmap *buffer,
blendColors(backgroundColor, blendColors(backgroundColor,
*ctx.colorProvider.color(ColorType::RedeemedHighlight)); *ctx.colorProvider.color(ColorType::RedeemedHighlight));
} }
else if (this->message_->flags.has(MessageFlag::AutoMod)) else if (this->message_->flags.has(MessageFlag::AutoMod) ||
this->message_->flags.has(MessageFlag::LowTrustUsers))
{ {
backgroundColor = QColor("#404040"); backgroundColor = QColor("#404040");
} }

View file

@ -7,6 +7,7 @@
#include "providers/twitch/PubSubHelpers.hpp" #include "providers/twitch/PubSubHelpers.hpp"
#include "providers/twitch/PubSubMessages.hpp" #include "providers/twitch/PubSubMessages.hpp"
#include "providers/twitch/TwitchAccount.hpp" #include "providers/twitch/TwitchAccount.hpp"
#include "pubsubmessages/LowTrustUsers.hpp"
#include "util/DebugCount.hpp" #include "util/DebugCount.hpp"
#include "util/Helpers.hpp" #include "util/Helpers.hpp"
#include "util/RapidjsonHelpers.hpp" #include "util/RapidjsonHelpers.hpp"
@ -585,6 +586,25 @@ void PubSub::unlistenAutomod()
} }
} }
void PubSub::unlistenLowTrustUsers()
{
for (const auto &p : this->clients)
{
const auto &client = p.second;
if (const auto &[topics, nonce] =
client->unlistenPrefix("low-trust-users.");
!topics.empty())
{
this->registerNonce(nonce, {
client,
"UNLISTEN",
topics,
topics.size(),
});
}
}
}
void PubSub::unlistenWhispers() void PubSub::unlistenWhispers()
{ {
for (const auto &p : this->clients) for (const auto &p : this->clients)
@ -670,6 +690,30 @@ void PubSub::listenToAutomod(const QString &channelID)
this->listenToTopic(topic); this->listenToTopic(topic);
} }
void PubSub::listenToLowTrustUsers(const QString &channelID)
{
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::listenToChannelPointRewards(const QString &channelID) void PubSub::listenToChannelPointRewards(const QString &channelID)
{ {
static const QString topicFormat("community-points-channel-v1.%1"); static const QString topicFormat("community-points-channel-v1.%1");
@ -1169,6 +1213,38 @@ void PubSub::handleMessageResponse(const PubSubMessageMessage &message)
this->signals_.moderation.autoModMessageCaught.invoke(innerMessage, this->signals_.moderation.autoModMessageCaught.invoke(innerMessage,
channelID); 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->signals_.moderation.suspiciousMessageReceived.invoke(
innerMessage);
}
break;
case PubSubLowTrustUsersMessage::Type::TreatmentUpdate: {
this->signals_.moderation.suspiciousTreatmentUpdated.invoke(
innerMessage);
}
break;
case PubSubLowTrustUsersMessage::Type::INVALID: {
qCWarning(chatterinoPubSub)
<< "Invalid low trust users event type:"
<< innerMessage.typeString;
}
break;
}
}
else else
{ {
qCDebug(chatterinoPubSub) << "Unknown topic:" << topic; qCDebug(chatterinoPubSub) << "Unknown topic:" << topic;

View file

@ -34,6 +34,7 @@ struct PubSubAutoModQueueMessage;
struct AutomodAction; struct AutomodAction;
struct AutomodUserAction; struct AutomodUserAction;
struct AutomodInfoAction; struct AutomodInfoAction;
struct PubSubLowTrustUsersMessage;
struct PubSubWhisperMessage; struct PubSubWhisperMessage;
struct PubSubListenMessage; struct PubSubListenMessage;
@ -67,9 +68,6 @@ class PubSub
QString userID_; QString userID_;
public: public:
// The max amount of connections we may open
static constexpr int maxConnections = 10;
PubSub(const QString &host, PubSub(const QString &host,
std::chrono::seconds pingInterval = std::chrono::seconds(15)); std::chrono::seconds pingInterval = std::chrono::seconds(15));
@ -100,6 +98,9 @@ public:
Signal<BanAction> userBanned; Signal<BanAction> userBanned;
Signal<UnbanAction> userUnbanned; Signal<UnbanAction> userUnbanned;
Signal<PubSubLowTrustUsersMessage> suspiciousMessageReceived;
Signal<PubSubLowTrustUsersMessage> suspiciousTreatmentUpdated;
// Message caught by automod // Message caught by automod
// channelID // channelID
pajlada::Signals::Signal<PubSubAutoModQueueMessage, QString> pajlada::Signals::Signal<PubSubAutoModQueueMessage, QString>
@ -126,12 +127,56 @@ public:
void unlistenAllModerationActions(); void unlistenAllModerationActions();
void unlistenAutomod(); void unlistenAutomod();
void unlistenLowTrustUsers();
void unlistenWhispers(); void unlistenWhispers();
/**
* Listen to incoming whispers for the currently logged in user.
* This topic is relevant for everyone.
*
* PubSub topic: whispers.{currentUserID}
*/
bool listenToWhispers(); bool listenToWhispers();
/**
* 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 listenToChannelModerationActions(const QString &channelID);
/**
* 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 listenToAutomod(const QString &channelID);
/**
* 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);
/**
* Listen to incoming channel point redemptions in the given channel.
* This topic is relevant for everyone.
*
* PubSub topic: community-points-channel-v1.{channelID}
*/
void listenToChannelPointRewards(const QString &channelID); void listenToChannelPointRewards(const QString &channelID);
std::vector<QString> requests; std::vector<QString> requests;

View file

@ -1253,7 +1253,11 @@ void TwitchChannel::refreshPubSub()
getApp()->twitch->pubsub->setAccount(currentAccount); getApp()->twitch->pubsub->setAccount(currentAccount);
getApp()->twitch->pubsub->listenToChannelModerationActions(roomId); getApp()->twitch->pubsub->listenToChannelModerationActions(roomId);
if (this->hasModRights())
{
getApp()->twitch->pubsub->listenToAutomod(roomId); getApp()->twitch->pubsub->listenToAutomod(roomId);
getApp()->twitch->pubsub->listenToLowTrustUsers(roomId);
}
getApp()->twitch->pubsub->listenToChannelPointRewards(roomId); getApp()->twitch->pubsub->listenToChannelPointRewards(roomId);
} }

View file

@ -25,13 +25,11 @@
#include <cassert> #include <cassert>
// using namespace Communi;
using namespace std::chrono_literals; using namespace std::chrono_literals;
#define TWITCH_PUBSUB_URL "wss://pubsub-edge.twitch.tv"
namespace { namespace {
const QString TWITCH_PUBSUB_URL = "wss://pubsub-edge.twitch.tv";
const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws"; const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws";
const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3"; const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3";
@ -45,11 +43,10 @@ TwitchIrcServer::TwitchIrcServer()
, liveChannel(new Channel("/live", Channel::Type::TwitchLive)) , liveChannel(new Channel("/live", Channel::Type::TwitchLive))
, automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod)) , automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod))
, watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching)
, pubsub(new PubSub(TWITCH_PUBSUB_URL))
{ {
this->initializeIrc(); this->initializeIrc();
this->pubsub = new PubSub(TWITCH_PUBSUB_URL);
if (getSettings()->enableBTTVLiveUpdates && if (getSettings()->enableBTTVLiveUpdates &&
getSettings()->enableBTTVChannelEmotes) getSettings()->enableBTTVChannelEmotes)
{ {

View file

@ -80,6 +80,7 @@ public:
const ChannelPtr automodChannel; const ChannelPtr automodChannel;
IndirectChannel watchingChannel; IndirectChannel watchingChannel;
// NOTE: We currently leak this
PubSub *pubsub; PubSub *pubsub;
std::unique_ptr<BttvLiveUpdates> bttvLiveUpdates; std::unique_ptr<BttvLiveUpdates> bttvLiveUpdates;
std::unique_ptr<SeventvEventAPI> seventvEventAPI; std::unique_ptr<SeventvEventAPI> seventvEventAPI;

View file

@ -1933,6 +1933,188 @@ std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeAutomodMessage(
return std::make_pair(message1, message2); return std::make_pair(message1, message2);
} }
MessagePtr TwitchMessageBuilder::makeLowTrustUpdateMessage(
const PubSubLowTrustUsersMessage &action)
{
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});
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);
}
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);
}
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);
}
break;
default:
qCDebug(chatterinoTwitch) << "Unexpected suspicious treatment: "
<< action.treatmentString;
break;
}
return builder.release();
}
std::pair<MessagePtr, MessagePtr> TwitchMessageBuilder::makeLowTrustUserMessage(
const PubSubLowTrustUsersMessage &action, const QString &channelName)
{
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";
}
else
{
headerMessage = "Monitored";
}
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 username
builder2
.emplace<TextElement>(action.suspiciousUserDisplayName + ":",
MessageElementFlag::BoldUsername,
MessageColor(action.suspiciousUserColor),
FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, action.suspiciousUserLogin});
builder2
.emplace<TextElement>(action.suspiciousUserDisplayName + ":",
MessageElementFlag::NonBoldUsername,
MessageColor(action.suspiciousUserColor))
->setLink({Link::UserInfo, action.suspiciousUserLogin});
// sender's message caught by AutoMod
builder2.emplace<TextElement>(action.text, MessageElementFlag::Text,
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);
}
void TwitchMessageBuilder::setThread(std::shared_ptr<MessageThread> thread) void TwitchMessageBuilder::setThread(std::shared_ptr<MessageThread> thread)
{ {
this->thread_ = std::move(thread); this->thread_ = std::move(thread);

View file

@ -3,6 +3,7 @@
#include "common/Aliases.hpp" #include "common/Aliases.hpp"
#include "common/Outcome.hpp" #include "common/Outcome.hpp"
#include "messages/SharedMessageBuilder.hpp" #include "messages/SharedMessageBuilder.hpp"
#include "pubsubmessages/LowTrustUsers.hpp"
#include <IrcMessage> #include <IrcMessage>
#include <QString> #include <QString>
@ -93,6 +94,11 @@ public:
const AutomodAction &action, const QString &channelName); const AutomodAction &action, const QString &channelName);
static MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action); static MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action);
static std::pair<MessagePtr, MessagePtr> makeLowTrustUserMessage(
const PubSubLowTrustUsersMessage &action, const QString &channelName);
static MessagePtr makeLowTrustUpdateMessage(
const PubSubLowTrustUsersMessage &action);
// Shares some common logic from SharedMessageBuilder::parseBadgeTag // Shares some common logic from SharedMessageBuilder::parseBadgeTag
static std::unordered_map<QString, QString> parseBadgeInfoTag( static std::unordered_map<QString, QString> parseBadgeInfoTag(
const QVariantMap &tags); const QVariantMap &tags);

View file

@ -0,0 +1,104 @@
#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp"
#include <QDateTime>
#include <QJsonArray>
namespace chatterino {
PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root)
: typeString(root.value("type").toString())
{
if (const auto oType =
magic_enum::enum_cast<Type>(this->typeString.toStdString());
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();
this->text =
data.value("message_content").toObject().value("text").toString();
// 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());
std::vector<LowTrustUserChatBadge> badges;
for (const auto &badge : sender.value("badges").toArray())
{
badges.emplace_back(badge.toObject());
}
this->senderBadges = badges;
const auto sharedValue = data.value("shared_ban_channel_ids");
std::vector<QString> sharedIDs;
if (!sharedValue.isNull())
{
for (const auto &id : sharedValue.toArray())
{
sharedIDs.emplace_back(id.toString());
}
}
this->sharedBanChannelIDs = sharedIDs;
}
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 = magic_enum::enum_cast<Treatment>(
this->treatmentString.toStdString());
oTreatment.has_value())
{
this->treatment = oTreatment.value();
}
this->evasionEvaluationString =
data.value("ban_evasion_evaluation").toString();
if (const auto oEvaluation = magic_enum::enum_cast<EvasionEvaluation>(
this->evasionEvaluationString.toStdString());
oEvaluation.has_value())
{
this->evasionEvaluation = oEvaluation.value();
}
FlagsEnum<RestrictionType> restrictions;
for (const auto &rType : data.value("types").toArray())
{
if (const auto oRestriction = magic_enum::enum_cast<RestrictionType>(
rType.toString().toStdString());
oRestriction.has_value())
{
restrictions.set(oRestriction.value());
}
}
this->restrictionTypes = restrictions;
}
} // namespace chatterino

View file

@ -0,0 +1,255 @@
#pragma once
#include <common/FlagsEnum.hpp>
#include <magic_enum/magic_enum.hpp>
#include <QColor>
#include <QJsonObject>
#include <QString>
namespace chatterino {
struct LowTrustUserChatBadge {
QString id;
QString version;
explicit LowTrustUserChatBadge(const QJsonObject &obj)
: id(obj.value("id").toString())
, version(obj.value("version").toString())
{
}
};
struct PubSubLowTrustUsersMessage {
/**
* 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;
/**
* 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<LowTrustUserChatBadge> 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;
}
}