diff --git a/CHANGELOG.md b/CHANGELOG.md index d1c04e5e75a..aedf2db88ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - 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: 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: 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) @@ -59,6 +60,7 @@ - Bugfix: Fixes to section deletion in text input fields. (#5013) - 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 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: Change clang-format from v14 to v16. (#4929) - Dev: Fixed UTF16 encoding of `modes` file for the installer. (#4791) diff --git a/src/Application.cpp b/src/Application.cpp index cfb28f13d85..5d6e4c63d04 100644 --- a/src/Application.cpp +++ b/src/Application.cpp @@ -35,6 +35,7 @@ #include "providers/twitch/PubSubActions.hpp" #include "providers/twitch/PubSubManager.hpp" #include "providers/twitch/PubSubMessages.hpp" +#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp" #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.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 = this->twitch->pubsub->signals_.moderation.autoModMessageCaught.connect( [&](const auto &msg, const QString &channelID) { @@ -672,6 +754,7 @@ void Application::initPubSub() [this] { this->twitch->pubsub->unlistenAllModerationActions(); this->twitch->pubsub->unlistenAutomod(); + this->twitch->pubsub->unlistenLowTrustUsers(); this->twitch->pubsub->unlistenWhispers(); }, boost::signals2::at_front); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f16ea09cf15..74a936ec18a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -412,6 +412,8 @@ set(SOURCE_FILES 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 diff --git a/src/common/Channel.cpp b/src/common/Channel.cpp index 47ae99c1c2e..cd275c14af3 100644 --- a/src/common/Channel.cpp +++ b/src/common/Channel.cpp @@ -315,7 +315,6 @@ bool Channel::isBroadcaster() const bool Channel::hasModRights() const { - // fourtf: check if staff return this->isMod() || this->isBroadcaster(); } diff --git a/src/messages/Message.hpp b/src/messages/Message.hpp index 88d0d09345c..30c1308a437 100644 --- a/src/messages/Message.hpp +++ b/src/messages/Message.hpp @@ -52,6 +52,7 @@ enum class MessageFlag : int64_t { LiveUpdatesUpdate = (1LL << 30), /// The message caught by AutoMod containing the user who sent the message & its contents AutoModOffendingMessage = (1LL << 31), + LowTrustUsers = (1LL << 32), }; using MessageFlags = FlagsEnum; diff --git a/src/messages/layouts/MessageLayout.cpp b/src/messages/layouts/MessageLayout.cpp index 0e8228485e9..c2e8d1886f2 100644 --- a/src/messages/layouts/MessageLayout.cpp +++ b/src/messages/layouts/MessageLayout.cpp @@ -367,7 +367,8 @@ void MessageLayout::updateBuffer(QPixmap *buffer, blendColors(backgroundColor, *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"); } diff --git a/src/providers/twitch/PubSubManager.cpp b/src/providers/twitch/PubSubManager.cpp index 80727ea4b8d..1dbe41392b5 100644 --- a/src/providers/twitch/PubSubManager.cpp +++ b/src/providers/twitch/PubSubManager.cpp @@ -7,6 +7,7 @@ #include "providers/twitch/PubSubHelpers.hpp" #include "providers/twitch/PubSubMessages.hpp" #include "providers/twitch/TwitchAccount.hpp" +#include "pubsubmessages/LowTrustUsers.hpp" #include "util/DebugCount.hpp" #include "util/Helpers.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() { for (const auto &p : this->clients) @@ -670,6 +690,30 @@ void PubSub::listenToAutomod(const QString &channelID) 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) { 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, channelID); } + else if (topic.startsWith("low-trust-users.")) + { + auto oInnerMessage = message.toInner(); + 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 { qCDebug(chatterinoPubSub) << "Unknown topic:" << topic; diff --git a/src/providers/twitch/PubSubManager.hpp b/src/providers/twitch/PubSubManager.hpp index f300bb5d338..f0701101f9b 100644 --- a/src/providers/twitch/PubSubManager.hpp +++ b/src/providers/twitch/PubSubManager.hpp @@ -34,6 +34,7 @@ struct PubSubAutoModQueueMessage; struct AutomodAction; struct AutomodUserAction; struct AutomodInfoAction; +struct PubSubLowTrustUsersMessage; struct PubSubWhisperMessage; struct PubSubListenMessage; @@ -67,9 +68,6 @@ class PubSub QString userID_; public: - // The max amount of connections we may open - static constexpr int maxConnections = 10; - PubSub(const QString &host, std::chrono::seconds pingInterval = std::chrono::seconds(15)); @@ -100,6 +98,9 @@ class PubSub Signal userBanned; Signal userUnbanned; + Signal suspiciousMessageReceived; + Signal suspiciousTreatmentUpdated; + // Message caught by automod // channelID pajlada::Signals::Signal @@ -126,12 +127,56 @@ class PubSub void unlistenAllModerationActions(); void unlistenAutomod(); + void unlistenLowTrustUsers(); void unlistenWhispers(); + /** + * Listen to incoming whispers for the currently logged in user. + * This topic is relevant for everyone. + * + * PubSub topic: whispers.{currentUserID} + */ 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); + + /** + * 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); + /** + * 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); std::vector requests; diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index ba25e6f79e3..8ee4ebbafbd 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1253,7 +1253,11 @@ void TwitchChannel::refreshPubSub() getApp()->twitch->pubsub->setAccount(currentAccount); getApp()->twitch->pubsub->listenToChannelModerationActions(roomId); - getApp()->twitch->pubsub->listenToAutomod(roomId); + if (this->hasModRights()) + { + getApp()->twitch->pubsub->listenToAutomod(roomId); + getApp()->twitch->pubsub->listenToLowTrustUsers(roomId); + } getApp()->twitch->pubsub->listenToChannelPointRewards(roomId); } diff --git a/src/providers/twitch/TwitchIrcServer.cpp b/src/providers/twitch/TwitchIrcServer.cpp index 84be99831ad..9602ce97618 100644 --- a/src/providers/twitch/TwitchIrcServer.cpp +++ b/src/providers/twitch/TwitchIrcServer.cpp @@ -25,13 +25,11 @@ #include -// using namespace Communi; using namespace std::chrono_literals; -#define TWITCH_PUBSUB_URL "wss://pubsub-edge.twitch.tv" - namespace { +const QString TWITCH_PUBSUB_URL = "wss://pubsub-edge.twitch.tv"; const QString BTTV_LIVE_UPDATES_URL = "wss://sockets.betterttv.net/ws"; const QString SEVENTV_EVENTAPI_URL = "wss://events.7tv.io/v3"; @@ -45,11 +43,10 @@ TwitchIrcServer::TwitchIrcServer() , liveChannel(new Channel("/live", Channel::Type::TwitchLive)) , automodChannel(new Channel("/automod", Channel::Type::TwitchAutomod)) , watchingChannel(Channel::getEmpty(), Channel::Type::TwitchWatching) + , pubsub(new PubSub(TWITCH_PUBSUB_URL)) { this->initializeIrc(); - this->pubsub = new PubSub(TWITCH_PUBSUB_URL); - if (getSettings()->enableBTTVLiveUpdates && getSettings()->enableBTTVChannelEmotes) { diff --git a/src/providers/twitch/TwitchIrcServer.hpp b/src/providers/twitch/TwitchIrcServer.hpp index 61fe8ddb3b3..f7f047de7e5 100644 --- a/src/providers/twitch/TwitchIrcServer.hpp +++ b/src/providers/twitch/TwitchIrcServer.hpp @@ -80,6 +80,7 @@ class TwitchIrcServer final : public AbstractIrcServer, const ChannelPtr automodChannel; IndirectChannel watchingChannel; + // NOTE: We currently leak this PubSub *pubsub; std::unique_ptr bttvLiveUpdates; std::unique_ptr seventvEventAPI; diff --git a/src/providers/twitch/TwitchMessageBuilder.cpp b/src/providers/twitch/TwitchMessageBuilder.cpp index e5f003a05a3..0035f52c337 100644 --- a/src/providers/twitch/TwitchMessageBuilder.cpp +++ b/src/providers/twitch/TwitchMessageBuilder.cpp @@ -1933,6 +1933,188 @@ std::pair TwitchMessageBuilder::makeAutomodMessage( return std::make_pair(message1, message2); } +MessagePtr TwitchMessageBuilder::makeLowTrustUpdateMessage( + const PubSubLowTrustUsersMessage &action) +{ + MessageBuilder builder; + builder.emplace(); + builder.message().flags.set(MessageFlag::System); + builder.message().flags.set(MessageFlag::PubSub); + builder.message().flags.set(MessageFlag::DoNotTriggerNotification); + + builder + .emplace(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("removed", MessageElementFlag::Text, + MessageColor::System); + builder + .emplace(action.suspiciousUserDisplayName, + MessageElementFlag::Username, + MessageColor::System, + FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, action.suspiciousUserLogin}); + builder.emplace("from the suspicious user list.", + MessageElementFlag::Text, + MessageColor::System); + } + break; + + case PubSubLowTrustUsersMessage::Treatment::ActiveMonitoring: { + builder.emplace("added", MessageElementFlag::Text, + MessageColor::System); + builder + .emplace(action.suspiciousUserDisplayName, + MessageElementFlag::Username, + MessageColor::System, + FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, action.suspiciousUserLogin}); + builder.emplace("as a monitored suspicious chatter.", + MessageElementFlag::Text, + MessageColor::System); + } + break; + + case PubSubLowTrustUsersMessage::Treatment::Restricted: { + builder.emplace("added", MessageElementFlag::Text, + MessageColor::System); + builder + .emplace(action.suspiciousUserDisplayName, + MessageElementFlag::Username, + MessageColor::System, + FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, action.suspiciousUserLogin}); + builder.emplace("as a restricted suspicious chatter.", + MessageElementFlag::Text, + MessageColor::System); + } + break; + + default: + qCDebug(chatterinoTwitch) << "Unexpected suspicious treatment: " + << action.treatmentString; + break; + } + + return builder.release(); +} + +std::pair 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(makeAutoModBadge(), + MessageElementFlag::BadgeChannelAuthority); + + // Suspicious user header message + QString prefix = "Suspicious User:"; + builder.emplace(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(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("#" + channelName, + MessageElementFlag::ChannelName, + MessageColor::System) + ->setLink({Link::JumpToChannel, channelName}); + builder2.emplace(); + builder2.emplace(); + builder2.message().loginName = action.suspiciousUserLogin; + builder2.message().flags.set(MessageFlag::PubSub); + builder2.message().flags.set(MessageFlag::LowTrustUsers); + + // sender username + builder2 + .emplace(action.suspiciousUserDisplayName + ":", + MessageElementFlag::BoldUsername, + MessageColor(action.suspiciousUserColor), + FontStyle::ChatMediumBold) + ->setLink({Link::UserInfo, action.suspiciousUserLogin}); + builder2 + .emplace(action.suspiciousUserDisplayName + ":", + MessageElementFlag::NonBoldUsername, + MessageColor(action.suspiciousUserColor)) + ->setLink({Link::UserInfo, action.suspiciousUserLogin}); + + // sender's message caught by AutoMod + builder2.emplace(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 thread) { this->thread_ = std::move(thread); diff --git a/src/providers/twitch/TwitchMessageBuilder.hpp b/src/providers/twitch/TwitchMessageBuilder.hpp index 92f4dc3fc6b..9bffd8f7a8f 100644 --- a/src/providers/twitch/TwitchMessageBuilder.hpp +++ b/src/providers/twitch/TwitchMessageBuilder.hpp @@ -3,6 +3,7 @@ #include "common/Aliases.hpp" #include "common/Outcome.hpp" #include "messages/SharedMessageBuilder.hpp" +#include "pubsubmessages/LowTrustUsers.hpp" #include #include @@ -93,6 +94,11 @@ class TwitchMessageBuilder : public SharedMessageBuilder const AutomodAction &action, const QString &channelName); static MessagePtr makeAutomodInfoMessage(const AutomodInfoAction &action); + static std::pair makeLowTrustUserMessage( + const PubSubLowTrustUsersMessage &action, const QString &channelName); + static MessagePtr makeLowTrustUpdateMessage( + const PubSubLowTrustUsersMessage &action); + // Shares some common logic from SharedMessageBuilder::parseBadgeTag static std::unordered_map parseBadgeInfoTag( const QVariantMap &tags); diff --git a/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp b/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp new file mode 100644 index 00000000000..630558944b6 --- /dev/null +++ b/src/providers/twitch/pubsubmessages/LowTrustUsers.cpp @@ -0,0 +1,104 @@ +#include "providers/twitch/pubsubmessages/LowTrustUsers.hpp" + +#include +#include + +namespace chatterino { + +PubSubLowTrustUsersMessage::PubSubLowTrustUsersMessage(const QJsonObject &root) + : typeString(root.value("type").toString()) +{ + if (const auto oType = + magic_enum::enum_cast(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 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 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( + 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( + this->evasionEvaluationString.toStdString()); + oEvaluation.has_value()) + { + this->evasionEvaluation = oEvaluation.value(); + } + + FlagsEnum restrictions; + for (const auto &rType : data.value("types").toArray()) + { + if (const auto oRestriction = magic_enum::enum_cast( + rType.toString().toStdString()); + oRestriction.has_value()) + { + restrictions.set(oRestriction.value()); + } + } + this->restrictionTypes = restrictions; +} + +} // namespace chatterino diff --git a/src/providers/twitch/pubsubmessages/LowTrustUsers.hpp b/src/providers/twitch/pubsubmessages/LowTrustUsers.hpp new file mode 100644 index 00000000000..84ca577d791 --- /dev/null +++ b/src/providers/twitch/pubsubmessages/LowTrustUsers.hpp @@ -0,0 +1,255 @@ +#pragma once + +#include +#include +#include +#include +#include + +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 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 sharedBanChannelIDs; + + /** + * A list of badges of the user who sent the message. + * Only used for the UserMessage type. + */ + std::vector 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; + } +}