diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f8f2e33..622d83ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,7 +30,7 @@ - Bugfix: Fixed the input font not immediately updating when zooming in/out. (#5960) - Bugfix: Fixed color input thinking blue is also red. (#5982) - Dev: Subscriptions to PubSub channel points redemption topics now use no auth token, making it continue to work during PubSub shutdown. (#5947) -- Dev: Add initial experimental EventSub support. (#5837, #5895, #5897, #5904, #5910, #5903, #5915, #5916, #5930, #5935, #5932, #5943, #5952, #5953, #5968, #5973, #5974, #5980, #5981, #5985, #5990, #5992, #5993, #5996, #5995, #6000, #6001, #6002, #6003, #6005) +- Dev: Add initial experimental EventSub support. (#5837, #5895, #5897, #5904, #5910, #5903, #5915, #5916, #5930, #5935, #5932, #5943, #5952, #5953, #5968, #5973, #5974, #5980, #5981, #5985, #5990, #5992, #5993, #5996, #5995, #6000, #6001, #6002, #6003, #6005, #6007) - Dev: Remove unneeded platform specifier for toasts. (#5914) - Dev: Highlight checks now use non-capturing groups for the boundaries. (#5784) - Dev: Removed unused PubSub whisper code. (#5898) diff --git a/lib/twitch-eventsub-ws/ast/lib/comment_commands.py b/lib/twitch-eventsub-ws/ast/lib/comment_commands.py index 390e59f1..4004c1d5 100644 --- a/lib/twitch-eventsub-ws/ast/lib/comment_commands.py +++ b/lib/twitch-eventsub-ws/ast/lib/comment_commands.py @@ -25,6 +25,8 @@ class CommentCommands: inner_root: str = "" + default: Optional[str] = None + def __init__(self, parent: Optional["CommentCommands"] = None) -> None: if parent is not None: self.name_transform = parent.name_transform @@ -59,6 +61,8 @@ class CommentCommands: pass case "json_tag": self.tag = value + case "default": + self.default = value case other: log.warning(f"Unknown comment command found: {other} with value {value}") diff --git a/lib/twitch-eventsub-ws/ast/lib/enum.py b/lib/twitch-eventsub-ws/ast/lib/enum.py index 56ab4dee..0eb5df69 100644 --- a/lib/twitch-eventsub-ws/ast/lib/enum.py +++ b/lib/twitch-eventsub-ws/ast/lib/enum.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import List +from typing import List, Optional import logging @@ -20,6 +20,7 @@ class Enum: self.comment_commands = CommentCommands() self.inner_root: str = "" self.namespace = namespace + self.default: Optional[str] = None @property def full_name(self) -> str: @@ -48,3 +49,4 @@ class Enum: def apply_comment_commands(self, comment_commands: CommentCommands) -> None: self.inner_root = comment_commands.inner_root + self.default = comment_commands.default diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/enum-implementation.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/enum-implementation.tmpl index 07d8cf69..e890e005 100644 --- a/lib/twitch-eventsub-ws/ast/lib/templates/enum-implementation.tmpl +++ b/lib/twitch-eventsub-ws/ast/lib/templates/enum-implementation.tmpl @@ -15,5 +15,9 @@ boost::json::result_for<{{enum.full_name}}, boost::json::value>::type tag_invoke } {%- endfor %} +{%- if enum.default -%} + return {{enum.full_name}}::{{enum.default}}; +{%- else -%} EVENTSUB_BAIL_HERE(error::Kind::UnknownEnumValue); +{%- endif -%} } diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-suspicious-user-message-v1.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-suspicious-user-message-v1.hpp index 6d8c2788..7c42d350 100644 --- a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-suspicious-user-message-v1.hpp +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-suspicious-user-message-v1.hpp @@ -2,35 +2,31 @@ #include "twitch-eventsub-ws/payloads/structured-message.hpp" #include "twitch-eventsub-ws/payloads/subscription.hpp" +#include "twitch-eventsub-ws/payloads/suspicious-users.hpp" #include -#include - namespace chatterino::eventsub::lib::payload::channel_suspicious_user_message:: v1 { struct Event { // Broadcaster of the channel the message was sent in - std::string broadcasterUserID; - std::string broadcasterUserLogin; - std::string broadcasterUserName; + String broadcasterUserID; + String broadcasterUserLogin; + String broadcasterUserName; // User who sent the message - std::string userID; - std::string userLogin; - std::string userName; + String userID; + String userLogin; + String userName; - // "none", "active_monitoring", "restricted" - std::string lowTrustStatus; + suspicious_users::Status lowTrustStatus; - std::vector sharedBanChannelIds; + std::vector sharedBanChannelIds; - // "manual", "ban_evader_detector", or "shared_channel_ban" - std::vector types; + std::vector types; - // "unknown", "possible", "likely" - std::string banEvasionEvaluation; + suspicious_users::BanEvasionEvaluation banEvasionEvaluation; // this event also has the ID in this message (hopefully we don't need it) chat::Message message; }; diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-suspicious-user-update-v1.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-suspicious-user-update-v1.hpp index 86cdb87c..0d0976d0 100644 --- a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-suspicious-user-update-v1.hpp +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-suspicious-user-update-v1.hpp @@ -1,32 +1,31 @@ #pragma once #include "twitch-eventsub-ws/payloads/subscription.hpp" +#include "twitch-eventsub-ws/payloads/suspicious-users.hpp" +#include "twitch-eventsub-ws/string.hpp" #include -#include - namespace chatterino::eventsub::lib::payload::channel_suspicious_user_update:: v1 { struct Event { // Broadcaster of the channel - std::string broadcasterUserID; - std::string broadcasterUserLogin; - std::string broadcasterUserName; + String broadcasterUserID; + String broadcasterUserLogin; + String broadcasterUserName; // Affected user - std::string userID; - std::string userLogin; - std::string userName; + String userID; + String userLogin; + String userName; // Moderator who updated the user - std::string moderatorUserID; - std::string moderatorUserLogin; - std::string moderatorUserName; + String moderatorUserID; + String moderatorUserLogin; + String moderatorUserName; - // "none", "active_monitoring", or "restricted" - std::string lowTrustStatus; + suspicious_users::Status lowTrustStatus; }; struct Payload { diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/suspicious-users.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/suspicious-users.hpp new file mode 100644 index 00000000..be015644 --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/suspicious-users.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +namespace chatterino::eventsub::lib::suspicious_users { + +/// default=None +enum class Status : std::uint8_t { + None, + ActiveMonitoring, + Restricted, +}; + +/// default=Unknown +enum class Type : std::uint8_t { + Unknown, + Manual, + BanEvaderDetector, + SharedChannelBan +}; + +/// default=Unknown +enum class BanEvasionEvaluation : std::uint8_t { + Unknown, + Possible, + Likely, +}; + +#include "twitch-eventsub-ws/payloads/suspicious-users.inc" + +} // namespace chatterino::eventsub::lib::suspicious_users diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/suspicious-users.inc b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/suspicious-users.inc new file mode 100644 index 00000000..de150aae --- /dev/null +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/suspicious-users.inc @@ -0,0 +1,9 @@ +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag, const boost::json::value &jvRoot); + +boost::json::result_for::type + tag_invoke(boost::json::try_value_to_tag, + const boost::json::value &jvRoot); diff --git a/lib/twitch-eventsub-ws/src/CMakeLists.txt b/lib/twitch-eventsub-ws/src/CMakeLists.txt index 0c03b475..f44a134d 100644 --- a/lib/twitch-eventsub-ws/src/CMakeLists.txt +++ b/lib/twitch-eventsub-ws/src/CMakeLists.txt @@ -21,6 +21,7 @@ generate_json_impls( include/twitch-eventsub-ws/payloads/stream-online-v1.hpp include/twitch-eventsub-ws/payloads/structured-message.hpp include/twitch-eventsub-ws/payloads/subscription.hpp + include/twitch-eventsub-ws/payloads/suspicious-users.hpp ) set(SOURCE_FILES diff --git a/lib/twitch-eventsub-ws/src/generated/payloads/channel-suspicious-user-message-v1.cpp b/lib/twitch-eventsub-ws/src/generated/payloads/channel-suspicious-user-message-v1.cpp index 54b838fe..8a37a0c2 100644 --- a/lib/twitch-eventsub-ws/src/generated/payloads/channel-suspicious-user-message-v1.cpp +++ b/lib/twitch-eventsub-ws/src/generated/payloads/channel-suspicious-user-message-v1.cpp @@ -26,7 +26,7 @@ boost::json::result_for::type tag_invoke( } auto broadcasterUserID = - boost::json::try_value_to(*jvbroadcasterUserID); + boost::json::try_value_to(*jvbroadcasterUserID); if (broadcasterUserID.has_error()) { @@ -41,7 +41,7 @@ boost::json::result_for::type tag_invoke( } auto broadcasterUserLogin = - boost::json::try_value_to(*jvbroadcasterUserLogin); + boost::json::try_value_to(*jvbroadcasterUserLogin); if (broadcasterUserLogin.has_error()) { @@ -56,7 +56,7 @@ boost::json::result_for::type tag_invoke( } auto broadcasterUserName = - boost::json::try_value_to(*jvbroadcasterUserName); + boost::json::try_value_to(*jvbroadcasterUserName); if (broadcasterUserName.has_error()) { @@ -69,7 +69,7 @@ boost::json::result_for::type tag_invoke( EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); } - auto userID = boost::json::try_value_to(*jvuserID); + auto userID = boost::json::try_value_to(*jvuserID); if (userID.has_error()) { @@ -82,7 +82,7 @@ boost::json::result_for::type tag_invoke( EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); } - auto userLogin = boost::json::try_value_to(*jvuserLogin); + auto userLogin = boost::json::try_value_to(*jvuserLogin); if (userLogin.has_error()) { @@ -95,13 +95,15 @@ boost::json::result_for::type tag_invoke( EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); } - auto userName = boost::json::try_value_to(*jvuserName); + auto userName = boost::json::try_value_to(*jvuserName); if (userName.has_error()) { return userName.error(); } + static_assert(std::is_trivially_copyable_v().lowTrustStatus)>>); const auto *jvlowTrustStatus = root.if_contains("low_trust_status"); if (jvlowTrustStatus == nullptr) { @@ -109,21 +111,21 @@ boost::json::result_for::type tag_invoke( } auto lowTrustStatus = - boost::json::try_value_to(*jvlowTrustStatus); + boost::json::try_value_to(*jvlowTrustStatus); if (lowTrustStatus.has_error()) { return lowTrustStatus.error(); } - std::vector vsharedBanChannelIds; + std::vector vsharedBanChannelIds; const auto *jvsharedBanChannelIds = root.if_contains("shared_ban_channel_ids"); if (jvsharedBanChannelIds != nullptr && !jvsharedBanChannelIds->is_null()) { - auto sharedBanChannelIds = - boost::json::try_value_to>( - *jvsharedBanChannelIds); + auto sharedBanChannelIds = boost::json::try_value_to< + std::vector>( + *jvsharedBanChannelIds); if (sharedBanChannelIds.has_error()) { return sharedBanChannelIds.error(); @@ -134,12 +136,13 @@ boost::json::result_for::type tag_invoke( } } - std::vector vtypes; + std::vector vtypes; const auto *jvtypes = root.if_contains("types"); if (jvtypes != nullptr && !jvtypes->is_null()) { - auto types = - boost::json::try_value_to>(*jvtypes); + auto types = boost::json::try_value_to< + std::vector>( + *jvtypes); if (types.has_error()) { return types.error(); @@ -149,6 +152,8 @@ boost::json::result_for::type tag_invoke( vtypes = std::move(types.value()); } } + static_assert(std::is_trivially_copyable_v().banEvasionEvaluation)>>); const auto *jvbanEvasionEvaluation = root.if_contains("ban_evasion_evaluation"); if (jvbanEvasionEvaluation == nullptr) @@ -157,7 +162,8 @@ boost::json::result_for::type tag_invoke( } auto banEvasionEvaluation = - boost::json::try_value_to(*jvbanEvasionEvaluation); + boost::json::try_value_to( + *jvbanEvasionEvaluation); if (banEvasionEvaluation.has_error()) { @@ -184,10 +190,10 @@ boost::json::result_for::type tag_invoke( .userID = std::move(userID.value()), .userLogin = std::move(userLogin.value()), .userName = std::move(userName.value()), - .lowTrustStatus = std::move(lowTrustStatus.value()), + .lowTrustStatus = lowTrustStatus.value(), .sharedBanChannelIds = std::move(vsharedBanChannelIds), .types = std::move(vtypes), - .banEvasionEvaluation = std::move(banEvasionEvaluation.value()), + .banEvasionEvaluation = banEvasionEvaluation.value(), .message = std::move(message.value()), }; } diff --git a/lib/twitch-eventsub-ws/src/generated/payloads/channel-suspicious-user-update-v1.cpp b/lib/twitch-eventsub-ws/src/generated/payloads/channel-suspicious-user-update-v1.cpp index edddeae4..f78cf32f 100644 --- a/lib/twitch-eventsub-ws/src/generated/payloads/channel-suspicious-user-update-v1.cpp +++ b/lib/twitch-eventsub-ws/src/generated/payloads/channel-suspicious-user-update-v1.cpp @@ -26,7 +26,7 @@ boost::json::result_for::type tag_invoke( } auto broadcasterUserID = - boost::json::try_value_to(*jvbroadcasterUserID); + boost::json::try_value_to(*jvbroadcasterUserID); if (broadcasterUserID.has_error()) { @@ -41,7 +41,7 @@ boost::json::result_for::type tag_invoke( } auto broadcasterUserLogin = - boost::json::try_value_to(*jvbroadcasterUserLogin); + boost::json::try_value_to(*jvbroadcasterUserLogin); if (broadcasterUserLogin.has_error()) { @@ -56,7 +56,7 @@ boost::json::result_for::type tag_invoke( } auto broadcasterUserName = - boost::json::try_value_to(*jvbroadcasterUserName); + boost::json::try_value_to(*jvbroadcasterUserName); if (broadcasterUserName.has_error()) { @@ -69,7 +69,7 @@ boost::json::result_for::type tag_invoke( EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); } - auto userID = boost::json::try_value_to(*jvuserID); + auto userID = boost::json::try_value_to(*jvuserID); if (userID.has_error()) { @@ -82,7 +82,7 @@ boost::json::result_for::type tag_invoke( EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); } - auto userLogin = boost::json::try_value_to(*jvuserLogin); + auto userLogin = boost::json::try_value_to(*jvuserLogin); if (userLogin.has_error()) { @@ -95,7 +95,7 @@ boost::json::result_for::type tag_invoke( EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); } - auto userName = boost::json::try_value_to(*jvuserName); + auto userName = boost::json::try_value_to(*jvuserName); if (userName.has_error()) { @@ -109,7 +109,7 @@ boost::json::result_for::type tag_invoke( } auto moderatorUserID = - boost::json::try_value_to(*jvmoderatorUserID); + boost::json::try_value_to(*jvmoderatorUserID); if (moderatorUserID.has_error()) { @@ -123,7 +123,7 @@ boost::json::result_for::type tag_invoke( } auto moderatorUserLogin = - boost::json::try_value_to(*jvmoderatorUserLogin); + boost::json::try_value_to(*jvmoderatorUserLogin); if (moderatorUserLogin.has_error()) { @@ -137,13 +137,15 @@ boost::json::result_for::type tag_invoke( } auto moderatorUserName = - boost::json::try_value_to(*jvmoderatorUserName); + boost::json::try_value_to(*jvmoderatorUserName); if (moderatorUserName.has_error()) { return moderatorUserName.error(); } + static_assert(std::is_trivially_copyable_v().lowTrustStatus)>>); const auto *jvlowTrustStatus = root.if_contains("low_trust_status"); if (jvlowTrustStatus == nullptr) { @@ -151,7 +153,7 @@ boost::json::result_for::type tag_invoke( } auto lowTrustStatus = - boost::json::try_value_to(*jvlowTrustStatus); + boost::json::try_value_to(*jvlowTrustStatus); if (lowTrustStatus.has_error()) { @@ -168,7 +170,7 @@ boost::json::result_for::type tag_invoke( .moderatorUserID = std::move(moderatorUserID.value()), .moderatorUserLogin = std::move(moderatorUserLogin.value()), .moderatorUserName = std::move(moderatorUserName.value()), - .lowTrustStatus = std::move(lowTrustStatus.value()), + .lowTrustStatus = lowTrustStatus.value(), }; } diff --git a/lib/twitch-eventsub-ws/src/generated/payloads/suspicious-users.cpp b/lib/twitch-eventsub-ws/src/generated/payloads/suspicious-users.cpp new file mode 100644 index 00000000..bb2a8dca --- /dev/null +++ b/lib/twitch-eventsub-ws/src/generated/payloads/suspicious-users.cpp @@ -0,0 +1,93 @@ +// WARNING: This file is automatically generated. Any changes will be lost. +#include "twitch-eventsub-ws/chrono.hpp" // IWYU pragma: keep +#include "twitch-eventsub-ws/detail/errors.hpp" +#include "twitch-eventsub-ws/detail/variant.hpp" // IWYU pragma: keep +#include "twitch-eventsub-ws/payloads/suspicious-users.hpp" + +#include + +namespace chatterino::eventsub::lib::suspicious_users { + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag /* tag */, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_string()) + { + EVENTSUB_BAIL_HERE(error::Kind::ExpectedString); + } + std::string_view eString(jvRoot.get_string()); + + using namespace std::string_view_literals; + if (eString == "none"sv) + { + return Status::None; + } + if (eString == "active_monitoring"sv) + { + return Status::ActiveMonitoring; + } + if (eString == "restricted"sv) + { + return Status::Restricted; + } + return Status::None; +} + +boost::json::result_for::type tag_invoke( + boost::json::try_value_to_tag /* tag */, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_string()) + { + EVENTSUB_BAIL_HERE(error::Kind::ExpectedString); + } + std::string_view eString(jvRoot.get_string()); + + using namespace std::string_view_literals; + if (eString == "unknown"sv) + { + return Type::Unknown; + } + if (eString == "manual"sv) + { + return Type::Manual; + } + if (eString == "ban_evader_detector"sv) + { + return Type::BanEvaderDetector; + } + if (eString == "shared_channel_ban"sv) + { + return Type::SharedChannelBan; + } + return Type::Unknown; +} + +boost::json::result_for::type + tag_invoke(boost::json::try_value_to_tag /* tag */, + const boost::json::value &jvRoot) +{ + if (!jvRoot.is_string()) + { + EVENTSUB_BAIL_HERE(error::Kind::ExpectedString); + } + std::string_view eString(jvRoot.get_string()); + + using namespace std::string_view_literals; + if (eString == "unknown"sv) + { + return BanEvasionEvaluation::Unknown; + } + if (eString == "possible"sv) + { + return BanEvasionEvaluation::Possible; + } + if (eString == "likely"sv) + { + return BanEvasionEvaluation::Likely; + } + return BanEvasionEvaluation::Unknown; +} + +} // namespace chatterino::eventsub::lib::suspicious_users diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index 5d8230bd..88b14c15 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1522,10 +1522,27 @@ void TwitchChannel::refreshPubSub() }, }, }); + this->eventSubSuspiciousUserMessageHandle = + getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{ + .subscriptionType = "channel.suspicious_user.message", + .subscriptionVersion = "1", + .conditions = + { + { + "broadcaster_user_id", + roomId, + }, + { + "moderator_user_id", + currentAccount->getUserId(), + }, + }, + }); } else { this->eventSubChannelModerateHandle.reset(); + this->eventSubSuspiciousUserMessageHandle.reset(); } getApp()->getTwitchPubSub()->listenToChannelPointRewards(roomId); } diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 1832b41b..ed897792 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -510,6 +510,7 @@ private: eventsub::SubscriptionHandle eventSubChannelModerateHandle; eventsub::SubscriptionHandle eventSubAutomodMessageHoldHandle; + eventsub::SubscriptionHandle eventSubSuspiciousUserMessageHandle; friend class TwitchIrcServer; friend class MessageBuilder; diff --git a/src/providers/twitch/eventsub/Connection.cpp b/src/providers/twitch/eventsub/Connection.cpp index 7193502a..c273ecd0 100644 --- a/src/providers/twitch/eventsub/Connection.cpp +++ b/src/providers/twitch/eventsub/Connection.cpp @@ -12,6 +12,7 @@ #include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchIrcServer.hpp" #include "singletons/Settings.hpp" +#include "singletons/StreamerMode.hpp" #include "singletons/WindowManager.hpp" #include "util/Helpers.hpp" #include "util/PostToThread.hpp" @@ -243,9 +244,40 @@ void Connection::onChannelSuspiciousUserMessage( const lib::messages::Metadata &metadata, const lib::payload::channel_suspicious_user_message::v1::Payload &payload) { - (void)metadata; - qCDebug(LOG) << "On channel suspicious user message for" - << payload.event.broadcasterUserLogin.c_str(); + // monitored chats are received over irc; in the future, we will use eventsub instead + if (payload.event.lowTrustStatus != + lib::suspicious_users::Status::Restricted) + { + return; + } + + if (getSettings()->streamerModeHideModActions && + getApp()->getStreamerMode()->isEnabled()) + { + return; + } + + auto *channel = dynamic_cast( + getApp() + ->getTwitch() + ->getChannelOrEmpty(payload.event.broadcasterUserLogin.qt()) + .get()); + if (!channel || channel->isEmpty()) + { + qCDebug(LOG) + << "Suspicious message for broadcaster we're not interested in" + << payload.event.broadcasterUserLogin.qt(); + return; + } + + auto time = chronoToQDateTime(metadata.messageTimestamp); + auto header = makeSuspiciousUserMessageHeader(channel, time, payload.event); + auto body = makeSuspiciousUserMessageBody(channel, time, payload.event); + + runInGuiThread([channel, header, body] { + channel->addMessage(header, MessageContext::Original); + channel->addMessage(body, MessageContext::Original); + }); } void Connection::onChannelSuspiciousUserUpdate( @@ -254,7 +286,7 @@ void Connection::onChannelSuspiciousUserUpdate( { (void)metadata; qCDebug(LOG) << "On channel suspicious user update for" - << payload.event.broadcasterUserLogin.c_str(); + << payload.event.broadcasterUserLogin.qt(); } QString Connection::getSessionID() const diff --git a/src/providers/twitch/eventsub/MessageBuilder.cpp b/src/providers/twitch/eventsub/MessageBuilder.cpp index 7827cd94..a91be0d2 100644 --- a/src/providers/twitch/eventsub/MessageBuilder.cpp +++ b/src/providers/twitch/eventsub/MessageBuilder.cpp @@ -637,6 +637,7 @@ MessagePtr makeAutomodHoldMessageBody( displayName + ':', event.userLogin.qt(), MessageColor::Text, channel->getUserColor(event.userLogin.qt())); // sender's message caught by AutoMod + // XXX: add the structured message here builder.emplace(event.message.text.qt(), MessageElementFlag::Text, MessageColor::Text); auto text = displayName % u": " % event.message.text.qt(); @@ -646,4 +647,113 @@ MessagePtr makeAutomodHoldMessageBody( return builder.release(); } +MessagePtr makeSuspiciousUserMessageHeader( + TwitchChannel *channel, const QDateTime &time, + const lib::payload::channel_suspicious_user_message::v1::Event &event) +{ + EventSubMessageBuilder builder(channel); + + // Builder for low trust user message with explanation + builder->channelName = event.broadcasterUserLogin.qt(); + builder->serverReceivedTime = time; + builder->flags.set(MessageFlag::LowTrustUsers); + + // AutoMod shield badge + builder.emplace(makeAutoModBadge(), + MessageElementFlag::BadgeChannelAuthority); + + // Suspicious user header message + QString prefix = u"Suspicious User:"_s; + builder.emplace(prefix, MessageElementFlag::Text, + MessageColor(QColor(0, 0, 255)), + FontStyle::ChatMediumBold); + + QString headerMessage; + if (event.lowTrustStatus == lib::suspicious_users::Status::Restricted) + { + headerMessage = u"Restricted"_s; + } + else + { + headerMessage = u"Monitored"_s; + } + + auto hasType = [&](lib::suspicious_users::Type type) { + return std::ranges::find(event.types, type) != event.types.end(); + }; + + if (hasType(lib::suspicious_users::Type::BanEvaderDetector)) + { + QString evader; + if (event.banEvasionEvaluation == + lib::suspicious_users::BanEvasionEvaluation::Likely) + { + evader = u"likely"_s; + } + else + { + evader = u"possible"_s; + } + + headerMessage += u". Detected as " % evader % " ban evader"; + } + + if (hasType(lib::suspicious_users::Type::SharedChannelBan)) + { + headerMessage += u". Banned in " % + QString::number(event.sharedBanChannelIds.size()) % + u" shared channels"; + } + + builder.emplace(headerMessage, MessageElementFlag::Text, + MessageColor::Text); + builder->messageText = prefix % u" " % headerMessage; + builder->searchText = prefix % u" " % headerMessage; + + return builder.release(); +} + +MessagePtr makeSuspiciousUserMessageBody( + TwitchChannel *channel, const QDateTime &time, + const lib::payload::channel_suspicious_user_message::v1::Event &event) +{ + EventSubMessageBuilder builder(channel); + builder->channelName = event.broadcasterUserLogin.qt(); + builder->serverReceivedTime = time; + if (event.lowTrustStatus == lib::suspicious_users::Status::Restricted) + { + builder->flags.set(MessageFlag::RestrictedMessage); + } + else + { + builder->flags.set(MessageFlag::MonitoredMessage); + } + + builder + .emplace(u'#' + event.broadcasterUserLogin.qt(), + MessageElementFlag::ChannelName, + MessageColor::System) + ->setLink({Link::JumpToChannel, event.broadcasterUserLogin.qt()}); + builder.emplace(time.time()); + builder.emplace(); + builder->loginName = event.userLogin.qt(); + builder->flags.set(MessageFlag::PubSub, MessageFlag::LowTrustUsers); + + // sender username + builder.emplace( + event.userName.qt() + ":", event.userLogin.qt(), MessageColor::Text, + channel->getUserColor(event.userLogin.qt())); + + // sender's message caught by AutoMod + // XXX: add the structured message here + builder.emplace(event.message.text.qt(), + MessageElementFlag::Text, MessageColor::Text); + + auto text = event.userName.qt() % u": " % event.message.text.qt(); + builder.message().messageText = text; + builder.message().searchText = text; + + return builder.release(); +} + } // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/MessageBuilder.hpp b/src/providers/twitch/eventsub/MessageBuilder.hpp index 5377b05c..0e988b92 100644 --- a/src/providers/twitch/eventsub/MessageBuilder.hpp +++ b/src/providers/twitch/eventsub/MessageBuilder.hpp @@ -4,6 +4,7 @@ #include "providers/twitch/TwitchChannel.hpp" #include "twitch-eventsub-ws/payloads/automod-message-hold-v2.hpp" #include "twitch-eventsub-ws/payloads/channel-moderate-v2.hpp" +#include "twitch-eventsub-ws/payloads/channel-suspicious-user-message-v1.hpp" #include @@ -156,4 +157,12 @@ MessagePtr makeAutomodHoldMessageBody( TwitchChannel *channel, const QDateTime &time, const lib::payload::automod_message_hold::v2::Event &event); +MessagePtr makeSuspiciousUserMessageHeader( + TwitchChannel *channel, const QDateTime &time, + const lib::payload::channel_suspicious_user_message::v1::Event &event); + +MessagePtr makeSuspiciousUserMessageBody( + TwitchChannel *channel, const QDateTime &time, + const lib::payload::channel_suspicious_user_message::v1::Event &event); + } // namespace chatterino::eventsub diff --git a/tests/snapshots/EventSub/channel-suspicious-user-message/monitored.json b/tests/snapshots/EventSub/channel-suspicious-user-message/monitored.json new file mode 100644 index 00000000..b1bed57c --- /dev/null +++ b/tests/snapshots/EventSub/channel-suspicious-user-message/monitored.json @@ -0,0 +1,30 @@ +{ + "input": { + "ban_evasion_evaluation": "unknown", + "broadcaster_user_id": "11148817", + "broadcaster_user_login": "pajlada", + "broadcaster_user_name": "pajlada", + "low_trust_status": "monitored", + "message": { + "fragments": [ + { + "cheermote": null, + "emote": null, + "text": "FeelsDankMan", + "type": "text" + } + ], + "message_id": "4715b69c-a147-4ff8-bbb5-7c830544d809", + "text": "FeelsDankMan" + }, + "shared_ban_channel_ids": null, + "types": [ + "manually_added" + ], + "user_id": "129546453", + "user_login": "nerixyz", + "user_name": "nerixyz" + }, + "output": [ + ] +} diff --git a/tests/snapshots/EventSub/channel-suspicious-user-message/none.json b/tests/snapshots/EventSub/channel-suspicious-user-message/none.json new file mode 100644 index 00000000..f889a3c0 --- /dev/null +++ b/tests/snapshots/EventSub/channel-suspicious-user-message/none.json @@ -0,0 +1,30 @@ +{ + "input": { + "ban_evasion_evaluation": "unknown", + "broadcaster_user_id": "11148817", + "broadcaster_user_login": "pajlada", + "broadcaster_user_name": "pajlada", + "low_trust_status": "none", + "message": { + "fragments": [ + { + "cheermote": null, + "emote": null, + "text": "FeelsDankMan", + "type": "text" + } + ], + "message_id": "4715b69c-a147-4ff8-bbb5-7c830544d809", + "text": "FeelsDankMan" + }, + "shared_ban_channel_ids": null, + "types": [ + "manually_added" + ], + "user_id": "129546453", + "user_login": "nerixyz", + "user_name": "nerixyz" + }, + "output": [ + ] +} diff --git a/tests/snapshots/EventSub/channel-suspicious-user-message/restricted-emote.json b/tests/snapshots/EventSub/channel-suspicious-user-message/restricted-emote.json new file mode 100644 index 00000000..5d689469 --- /dev/null +++ b/tests/snapshots/EventSub/channel-suspicious-user-message/restricted-emote.json @@ -0,0 +1,209 @@ +{ + "input": { + "ban_evasion_evaluation": "unknown", + "broadcaster_user_id": "11148817", + "broadcaster_user_login": "pajlada", + "broadcaster_user_name": "pajlada", + "low_trust_status": "restricted", + "message": { + "fragments": [ + { + "cheermote": null, + "emote": { + "emote_set_id": "0", + "id": "25" + }, + "text": "Kappa", + "type": "emote" + } + ], + "message_id": "c7acaf8b-6694-4181-be68-5d79464b0001", + "text": "Kappa" + }, + "shared_ban_channel_ids": null, + "types": [ + "manually_added" + ], + "user_id": "129546453", + "user_login": "nerixyz", + "user_name": "nerixyz" + }, + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "emote": { + "homePage": "https://dashboard.twitch.tv/settings/moderation/automod", + "images": { + "1x": "" + }, + "name": "", + "tooltip": "AutoMod" + }, + "flags": "BadgeChannelAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "AutoMod", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff0000ff", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Suspicious", + "User:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Restricted" + ] + } + ], + "flags": "LowTrustUsers|EventSub", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "Suspicious User: Restricted", + "searchText": "Suspicious User: Restricted", + "serverReceivedTime": "2024-05-14T12:31:47Z", + "timeoutUser": "", + "userID": "", + "usernameColor": "#ff000000" + }, + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "12:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "12:31:47", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "Text", + "userLoginName": "nerixyz", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Kappa" + ] + } + ], + "flags": "PubSub|LowTrustUsers|RestrictedMessage|EventSub", + "id": "", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "nerixyz: Kappa", + "searchText": "nerixyz: Kappa", + "serverReceivedTime": "2024-05-14T12:31:47Z", + "timeoutUser": "", + "userID": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/snapshots/EventSub/channel-suspicious-user-message/restricted.json b/tests/snapshots/EventSub/channel-suspicious-user-message/restricted.json new file mode 100644 index 00000000..d89714a7 --- /dev/null +++ b/tests/snapshots/EventSub/channel-suspicious-user-message/restricted.json @@ -0,0 +1,206 @@ +{ + "input": { + "ban_evasion_evaluation": "unknown", + "broadcaster_user_id": "11148817", + "broadcaster_user_login": "pajlada", + "broadcaster_user_name": "pajlada", + "low_trust_status": "restricted", + "message": { + "fragments": [ + { + "cheermote": null, + "emote": null, + "text": "FeelsDankMan", + "type": "text" + } + ], + "message_id": "4715b69c-a147-4ff8-bbb5-7c830544d809", + "text": "FeelsDankMan" + }, + "shared_ban_channel_ids": null, + "types": [ + "manually_added" + ], + "user_id": "129546453", + "user_login": "nerixyz", + "user_name": "nerixyz" + }, + "output": [ + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "emote": { + "homePage": "https://dashboard.twitch.tv/settings/moderation/automod", + "images": { + "1x": "" + }, + "name": "", + "tooltip": "AutoMod" + }, + "flags": "BadgeChannelAuthority", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "AutoMod", + "trailingSpace": true, + "type": "BadgeElement" + }, + { + "color": "#ff0000ff", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMediumBold", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Suspicious", + "User:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "Restricted" + ] + } + ], + "flags": "LowTrustUsers|EventSub", + "id": "", + "localizedName": "", + "loginName": "", + "messageText": "Suspicious User: Restricted", + "searchText": "Suspicious User: Restricted", + "serverReceivedTime": "2024-05-14T12:31:47Z", + "timeoutUser": "", + "userID": "", + "usernameColor": "#ff000000" + }, + { + "badgeInfos": { + }, + "badges": [ + ], + "channelName": "pajlada", + "count": 1, + "displayName": "", + "elements": [ + { + "color": "System", + "flags": "ChannelName", + "link": { + "type": "JumpToChannel", + "value": "pajlada" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "#pajlada" + ] + }, + { + "element": { + "color": "System", + "flags": "Timestamp", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "12:31" + ] + }, + "flags": "Timestamp", + "format": "", + "link": { + "type": "None", + "value": "" + }, + "time": "12:31:47", + "tooltip": "", + "trailingSpace": true, + "type": "TimestampElement" + }, + { + "flags": "ModeratorTools", + "link": { + "type": "None", + "value": "" + }, + "tooltip": "", + "trailingSpace": true, + "type": "TwitchModerationElement" + }, + { + "color": "Text", + "fallbackColor": "Text", + "flags": "Text|Mention", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "MentionElement", + "userColor": "Text", + "userLoginName": "nerixyz", + "words": [ + "nerixyz:" + ] + }, + { + "color": "Text", + "flags": "Text", + "link": { + "type": "None", + "value": "" + }, + "style": "ChatMedium", + "tooltip": "", + "trailingSpace": true, + "type": "TextElement", + "words": [ + "FeelsDankMan" + ] + } + ], + "flags": "PubSub|LowTrustUsers|RestrictedMessage|EventSub", + "id": "", + "localizedName": "", + "loginName": "nerixyz", + "messageText": "nerixyz: FeelsDankMan", + "searchText": "nerixyz: FeelsDankMan", + "serverReceivedTime": "2024-05-14T12:31:47Z", + "timeoutUser": "", + "userID": "", + "usernameColor": "#ff000000" + } + ] +} diff --git a/tests/src/EventSubMessages.cpp b/tests/src/EventSubMessages.cpp index 95fb524e..e5cd6d41 100644 --- a/tests/src/EventSubMessages.cpp +++ b/tests/src/EventSubMessages.cpp @@ -82,6 +82,25 @@ const std::map "cost": 0 })", }, + { + "channel-suspicious-user-message", + R"({ + "id": "a3122e32-6498-4847-8675-109b9b94f29c", + "status": "enabled", + "type": "channel.suspicious_user.message", + "version": "1", + "condition": { + "broadcaster_user_id": "489584266", + "moderator_user_id": "489584266" + }, + "transport": { + "method":"websocket", + "session_id":"AgoQ59RRLw0mS6S000QtK8f54BIGY2VsbC1j" + }, + "created_at": "2025-02-28T15:55:37.85489173Z", + "cost": 0 + })", + }, }; class MockApplication : public mock::BaseApplication