feat(eventsub): implement suspicious user message (#6007)

This commit is contained in:
nerix
2025-03-01 13:48:01 +01:00
committed by GitHub
parent c5d4a3a69b
commit bced3c658d
22 changed files with 872 additions and 62 deletions
+1 -1
View File
@@ -30,7 +30,7 @@
- Bugfix: Fixed the input font not immediately updating when zooming in/out. (#5960) - Bugfix: Fixed the input font not immediately updating when zooming in/out. (#5960)
- Bugfix: Fixed color input thinking blue is also red. (#5982) - 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: 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: Remove unneeded platform specifier for toasts. (#5914)
- Dev: Highlight checks now use non-capturing groups for the boundaries. (#5784) - Dev: Highlight checks now use non-capturing groups for the boundaries. (#5784)
- Dev: Removed unused PubSub whisper code. (#5898) - Dev: Removed unused PubSub whisper code. (#5898)
@@ -25,6 +25,8 @@ class CommentCommands:
inner_root: str = "" inner_root: str = ""
default: Optional[str] = None
def __init__(self, parent: Optional["CommentCommands"] = None) -> None: def __init__(self, parent: Optional["CommentCommands"] = None) -> None:
if parent is not None: if parent is not None:
self.name_transform = parent.name_transform self.name_transform = parent.name_transform
@@ -59,6 +61,8 @@ class CommentCommands:
pass pass
case "json_tag": case "json_tag":
self.tag = value self.tag = value
case "default":
self.default = value
case other: case other:
log.warning(f"Unknown comment command found: {other} with value {value}") log.warning(f"Unknown comment command found: {other} with value {value}")
+3 -1
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import List from typing import List, Optional
import logging import logging
@@ -20,6 +20,7 @@ class Enum:
self.comment_commands = CommentCommands() self.comment_commands = CommentCommands()
self.inner_root: str = "" self.inner_root: str = ""
self.namespace = namespace self.namespace = namespace
self.default: Optional[str] = None
@property @property
def full_name(self) -> str: def full_name(self) -> str:
@@ -48,3 +49,4 @@ class Enum:
def apply_comment_commands(self, comment_commands: CommentCommands) -> None: def apply_comment_commands(self, comment_commands: CommentCommands) -> None:
self.inner_root = comment_commands.inner_root self.inner_root = comment_commands.inner_root
self.default = comment_commands.default
@@ -15,5 +15,9 @@ boost::json::result_for<{{enum.full_name}}, boost::json::value>::type tag_invoke
} }
{%- endfor %} {%- endfor %}
{%- if enum.default -%}
return {{enum.full_name}}::{{enum.default}};
{%- else -%}
EVENTSUB_BAIL_HERE(error::Kind::UnknownEnumValue); EVENTSUB_BAIL_HERE(error::Kind::UnknownEnumValue);
{%- endif -%}
} }
@@ -2,35 +2,31 @@
#include "twitch-eventsub-ws/payloads/structured-message.hpp" #include "twitch-eventsub-ws/payloads/structured-message.hpp"
#include "twitch-eventsub-ws/payloads/subscription.hpp" #include "twitch-eventsub-ws/payloads/subscription.hpp"
#include "twitch-eventsub-ws/payloads/suspicious-users.hpp"
#include <boost/json.hpp> #include <boost/json.hpp>
#include <string>
namespace chatterino::eventsub::lib::payload::channel_suspicious_user_message:: namespace chatterino::eventsub::lib::payload::channel_suspicious_user_message::
v1 { v1 {
struct Event { struct Event {
// Broadcaster of the channel the message was sent in // Broadcaster of the channel the message was sent in
std::string broadcasterUserID; String broadcasterUserID;
std::string broadcasterUserLogin; String broadcasterUserLogin;
std::string broadcasterUserName; String broadcasterUserName;
// User who sent the message // User who sent the message
std::string userID; String userID;
std::string userLogin; String userLogin;
std::string userName; String userName;
// "none", "active_monitoring", "restricted" suspicious_users::Status lowTrustStatus;
std::string lowTrustStatus;
std::vector<std::string> sharedBanChannelIds; std::vector<String> sharedBanChannelIds;
// "manual", "ban_evader_detector", or "shared_channel_ban" std::vector<suspicious_users::Type> types;
std::vector<std::string> types;
// "unknown", "possible", "likely" suspicious_users::BanEvasionEvaluation banEvasionEvaluation;
std::string banEvasionEvaluation;
// this event also has the ID in this message (hopefully we don't need it) // this event also has the ID in this message (hopefully we don't need it)
chat::Message message; chat::Message message;
}; };
@@ -1,32 +1,31 @@
#pragma once #pragma once
#include "twitch-eventsub-ws/payloads/subscription.hpp" #include "twitch-eventsub-ws/payloads/subscription.hpp"
#include "twitch-eventsub-ws/payloads/suspicious-users.hpp"
#include "twitch-eventsub-ws/string.hpp"
#include <boost/json.hpp> #include <boost/json.hpp>
#include <string>
namespace chatterino::eventsub::lib::payload::channel_suspicious_user_update:: namespace chatterino::eventsub::lib::payload::channel_suspicious_user_update::
v1 { v1 {
struct Event { struct Event {
// Broadcaster of the channel // Broadcaster of the channel
std::string broadcasterUserID; String broadcasterUserID;
std::string broadcasterUserLogin; String broadcasterUserLogin;
std::string broadcasterUserName; String broadcasterUserName;
// Affected user // Affected user
std::string userID; String userID;
std::string userLogin; String userLogin;
std::string userName; String userName;
// Moderator who updated the user // Moderator who updated the user
std::string moderatorUserID; String moderatorUserID;
std::string moderatorUserLogin; String moderatorUserLogin;
std::string moderatorUserName; String moderatorUserName;
// "none", "active_monitoring", or "restricted" suspicious_users::Status lowTrustStatus;
std::string lowTrustStatus;
}; };
struct Payload { struct Payload {
@@ -0,0 +1,31 @@
#pragma once
#include <cstdint>
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
@@ -0,0 +1,9 @@
boost::json::result_for<Status, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Status>, const boost::json::value &jvRoot);
boost::json::result_for<Type, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Type>, const boost::json::value &jvRoot);
boost::json::result_for<BanEvasionEvaluation, boost::json::value>::type
tag_invoke(boost::json::try_value_to_tag<BanEvasionEvaluation>,
const boost::json::value &jvRoot);
@@ -21,6 +21,7 @@ generate_json_impls(
include/twitch-eventsub-ws/payloads/stream-online-v1.hpp include/twitch-eventsub-ws/payloads/stream-online-v1.hpp
include/twitch-eventsub-ws/payloads/structured-message.hpp include/twitch-eventsub-ws/payloads/structured-message.hpp
include/twitch-eventsub-ws/payloads/subscription.hpp include/twitch-eventsub-ws/payloads/subscription.hpp
include/twitch-eventsub-ws/payloads/suspicious-users.hpp
) )
set(SOURCE_FILES set(SOURCE_FILES
@@ -26,7 +26,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto broadcasterUserID = auto broadcasterUserID =
boost::json::try_value_to<std::string>(*jvbroadcasterUserID); boost::json::try_value_to<String>(*jvbroadcasterUserID);
if (broadcasterUserID.has_error()) if (broadcasterUserID.has_error())
{ {
@@ -41,7 +41,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto broadcasterUserLogin = auto broadcasterUserLogin =
boost::json::try_value_to<std::string>(*jvbroadcasterUserLogin); boost::json::try_value_to<String>(*jvbroadcasterUserLogin);
if (broadcasterUserLogin.has_error()) if (broadcasterUserLogin.has_error())
{ {
@@ -56,7 +56,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto broadcasterUserName = auto broadcasterUserName =
boost::json::try_value_to<std::string>(*jvbroadcasterUserName); boost::json::try_value_to<String>(*jvbroadcasterUserName);
if (broadcasterUserName.has_error()) if (broadcasterUserName.has_error())
{ {
@@ -69,7 +69,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
} }
auto userID = boost::json::try_value_to<std::string>(*jvuserID); auto userID = boost::json::try_value_to<String>(*jvuserID);
if (userID.has_error()) if (userID.has_error())
{ {
@@ -82,7 +82,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
} }
auto userLogin = boost::json::try_value_to<std::string>(*jvuserLogin); auto userLogin = boost::json::try_value_to<String>(*jvuserLogin);
if (userLogin.has_error()) if (userLogin.has_error())
{ {
@@ -95,13 +95,15 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
} }
auto userName = boost::json::try_value_to<std::string>(*jvuserName); auto userName = boost::json::try_value_to<String>(*jvuserName);
if (userName.has_error()) if (userName.has_error())
{ {
return userName.error(); return userName.error();
} }
static_assert(std::is_trivially_copyable_v<std::remove_reference_t<
decltype(std::declval<Event>().lowTrustStatus)>>);
const auto *jvlowTrustStatus = root.if_contains("low_trust_status"); const auto *jvlowTrustStatus = root.if_contains("low_trust_status");
if (jvlowTrustStatus == nullptr) if (jvlowTrustStatus == nullptr)
{ {
@@ -109,21 +111,21 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto lowTrustStatus = auto lowTrustStatus =
boost::json::try_value_to<std::string>(*jvlowTrustStatus); boost::json::try_value_to<suspicious_users::Status>(*jvlowTrustStatus);
if (lowTrustStatus.has_error()) if (lowTrustStatus.has_error())
{ {
return lowTrustStatus.error(); return lowTrustStatus.error();
} }
std::vector<std::string> vsharedBanChannelIds; std::vector<chatterino::eventsub::lib::String> vsharedBanChannelIds;
const auto *jvsharedBanChannelIds = const auto *jvsharedBanChannelIds =
root.if_contains("shared_ban_channel_ids"); root.if_contains("shared_ban_channel_ids");
if (jvsharedBanChannelIds != nullptr && !jvsharedBanChannelIds->is_null()) if (jvsharedBanChannelIds != nullptr && !jvsharedBanChannelIds->is_null())
{ {
auto sharedBanChannelIds = auto sharedBanChannelIds = boost::json::try_value_to<
boost::json::try_value_to<std::vector<std::string>>( std::vector<chatterino::eventsub::lib::String>>(
*jvsharedBanChannelIds); *jvsharedBanChannelIds);
if (sharedBanChannelIds.has_error()) if (sharedBanChannelIds.has_error())
{ {
return sharedBanChannelIds.error(); return sharedBanChannelIds.error();
@@ -134,12 +136,13 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
} }
std::vector<std::string> vtypes; std::vector<chatterino::eventsub::lib::suspicious_users::Type> vtypes;
const auto *jvtypes = root.if_contains("types"); const auto *jvtypes = root.if_contains("types");
if (jvtypes != nullptr && !jvtypes->is_null()) if (jvtypes != nullptr && !jvtypes->is_null())
{ {
auto types = auto types = boost::json::try_value_to<
boost::json::try_value_to<std::vector<std::string>>(*jvtypes); std::vector<chatterino::eventsub::lib::suspicious_users::Type>>(
*jvtypes);
if (types.has_error()) if (types.has_error())
{ {
return types.error(); return types.error();
@@ -149,6 +152,8 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
vtypes = std::move(types.value()); vtypes = std::move(types.value());
} }
} }
static_assert(std::is_trivially_copyable_v<std::remove_reference_t<
decltype(std::declval<Event>().banEvasionEvaluation)>>);
const auto *jvbanEvasionEvaluation = const auto *jvbanEvasionEvaluation =
root.if_contains("ban_evasion_evaluation"); root.if_contains("ban_evasion_evaluation");
if (jvbanEvasionEvaluation == nullptr) if (jvbanEvasionEvaluation == nullptr)
@@ -157,7 +162,8 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto banEvasionEvaluation = auto banEvasionEvaluation =
boost::json::try_value_to<std::string>(*jvbanEvasionEvaluation); boost::json::try_value_to<suspicious_users::BanEvasionEvaluation>(
*jvbanEvasionEvaluation);
if (banEvasionEvaluation.has_error()) if (banEvasionEvaluation.has_error())
{ {
@@ -184,10 +190,10 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
.userID = std::move(userID.value()), .userID = std::move(userID.value()),
.userLogin = std::move(userLogin.value()), .userLogin = std::move(userLogin.value()),
.userName = std::move(userName.value()), .userName = std::move(userName.value()),
.lowTrustStatus = std::move(lowTrustStatus.value()), .lowTrustStatus = lowTrustStatus.value(),
.sharedBanChannelIds = std::move(vsharedBanChannelIds), .sharedBanChannelIds = std::move(vsharedBanChannelIds),
.types = std::move(vtypes), .types = std::move(vtypes),
.banEvasionEvaluation = std::move(banEvasionEvaluation.value()), .banEvasionEvaluation = banEvasionEvaluation.value(),
.message = std::move(message.value()), .message = std::move(message.value()),
}; };
} }
@@ -26,7 +26,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto broadcasterUserID = auto broadcasterUserID =
boost::json::try_value_to<std::string>(*jvbroadcasterUserID); boost::json::try_value_to<String>(*jvbroadcasterUserID);
if (broadcasterUserID.has_error()) if (broadcasterUserID.has_error())
{ {
@@ -41,7 +41,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto broadcasterUserLogin = auto broadcasterUserLogin =
boost::json::try_value_to<std::string>(*jvbroadcasterUserLogin); boost::json::try_value_to<String>(*jvbroadcasterUserLogin);
if (broadcasterUserLogin.has_error()) if (broadcasterUserLogin.has_error())
{ {
@@ -56,7 +56,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto broadcasterUserName = auto broadcasterUserName =
boost::json::try_value_to<std::string>(*jvbroadcasterUserName); boost::json::try_value_to<String>(*jvbroadcasterUserName);
if (broadcasterUserName.has_error()) if (broadcasterUserName.has_error())
{ {
@@ -69,7 +69,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
} }
auto userID = boost::json::try_value_to<std::string>(*jvuserID); auto userID = boost::json::try_value_to<String>(*jvuserID);
if (userID.has_error()) if (userID.has_error())
{ {
@@ -82,7 +82,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
} }
auto userLogin = boost::json::try_value_to<std::string>(*jvuserLogin); auto userLogin = boost::json::try_value_to<String>(*jvuserLogin);
if (userLogin.has_error()) if (userLogin.has_error())
{ {
@@ -95,7 +95,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
} }
auto userName = boost::json::try_value_to<std::string>(*jvuserName); auto userName = boost::json::try_value_to<String>(*jvuserName);
if (userName.has_error()) if (userName.has_error())
{ {
@@ -109,7 +109,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto moderatorUserID = auto moderatorUserID =
boost::json::try_value_to<std::string>(*jvmoderatorUserID); boost::json::try_value_to<String>(*jvmoderatorUserID);
if (moderatorUserID.has_error()) if (moderatorUserID.has_error())
{ {
@@ -123,7 +123,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto moderatorUserLogin = auto moderatorUserLogin =
boost::json::try_value_to<std::string>(*jvmoderatorUserLogin); boost::json::try_value_to<String>(*jvmoderatorUserLogin);
if (moderatorUserLogin.has_error()) if (moderatorUserLogin.has_error())
{ {
@@ -137,13 +137,15 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto moderatorUserName = auto moderatorUserName =
boost::json::try_value_to<std::string>(*jvmoderatorUserName); boost::json::try_value_to<String>(*jvmoderatorUserName);
if (moderatorUserName.has_error()) if (moderatorUserName.has_error())
{ {
return moderatorUserName.error(); return moderatorUserName.error();
} }
static_assert(std::is_trivially_copyable_v<std::remove_reference_t<
decltype(std::declval<Event>().lowTrustStatus)>>);
const auto *jvlowTrustStatus = root.if_contains("low_trust_status"); const auto *jvlowTrustStatus = root.if_contains("low_trust_status");
if (jvlowTrustStatus == nullptr) if (jvlowTrustStatus == nullptr)
{ {
@@ -151,7 +153,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
} }
auto lowTrustStatus = auto lowTrustStatus =
boost::json::try_value_to<std::string>(*jvlowTrustStatus); boost::json::try_value_to<suspicious_users::Status>(*jvlowTrustStatus);
if (lowTrustStatus.has_error()) if (lowTrustStatus.has_error())
{ {
@@ -168,7 +170,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
.moderatorUserID = std::move(moderatorUserID.value()), .moderatorUserID = std::move(moderatorUserID.value()),
.moderatorUserLogin = std::move(moderatorUserLogin.value()), .moderatorUserLogin = std::move(moderatorUserLogin.value()),
.moderatorUserName = std::move(moderatorUserName.value()), .moderatorUserName = std::move(moderatorUserName.value()),
.lowTrustStatus = std::move(lowTrustStatus.value()), .lowTrustStatus = lowTrustStatus.value(),
}; };
} }
@@ -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 <boost/json.hpp>
namespace chatterino::eventsub::lib::suspicious_users {
boost::json::result_for<Status, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Status> /* 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, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Type> /* 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<BanEvasionEvaluation, boost::json::value>::type
tag_invoke(boost::json::try_value_to_tag<BanEvasionEvaluation> /* 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
+17
View File
@@ -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 else
{ {
this->eventSubChannelModerateHandle.reset(); this->eventSubChannelModerateHandle.reset();
this->eventSubSuspiciousUserMessageHandle.reset();
} }
getApp()->getTwitchPubSub()->listenToChannelPointRewards(roomId); getApp()->getTwitchPubSub()->listenToChannelPointRewards(roomId);
} }
+1
View File
@@ -510,6 +510,7 @@ private:
eventsub::SubscriptionHandle eventSubChannelModerateHandle; eventsub::SubscriptionHandle eventSubChannelModerateHandle;
eventsub::SubscriptionHandle eventSubAutomodMessageHoldHandle; eventsub::SubscriptionHandle eventSubAutomodMessageHoldHandle;
eventsub::SubscriptionHandle eventSubSuspiciousUserMessageHandle;
friend class TwitchIrcServer; friend class TwitchIrcServer;
friend class MessageBuilder; friend class MessageBuilder;
+36 -4
View File
@@ -12,6 +12,7 @@
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp" #include "providers/twitch/TwitchIrcServer.hpp"
#include "singletons/Settings.hpp" #include "singletons/Settings.hpp"
#include "singletons/StreamerMode.hpp"
#include "singletons/WindowManager.hpp" #include "singletons/WindowManager.hpp"
#include "util/Helpers.hpp" #include "util/Helpers.hpp"
#include "util/PostToThread.hpp" #include "util/PostToThread.hpp"
@@ -243,9 +244,40 @@ void Connection::onChannelSuspiciousUserMessage(
const lib::messages::Metadata &metadata, const lib::messages::Metadata &metadata,
const lib::payload::channel_suspicious_user_message::v1::Payload &payload) const lib::payload::channel_suspicious_user_message::v1::Payload &payload)
{ {
(void)metadata; // monitored chats are received over irc; in the future, we will use eventsub instead
qCDebug(LOG) << "On channel suspicious user message for" if (payload.event.lowTrustStatus !=
<< payload.event.broadcasterUserLogin.c_str(); lib::suspicious_users::Status::Restricted)
{
return;
}
if (getSettings()->streamerModeHideModActions &&
getApp()->getStreamerMode()->isEnabled())
{
return;
}
auto *channel = dynamic_cast<TwitchChannel *>(
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( void Connection::onChannelSuspiciousUserUpdate(
@@ -254,7 +286,7 @@ void Connection::onChannelSuspiciousUserUpdate(
{ {
(void)metadata; (void)metadata;
qCDebug(LOG) << "On channel suspicious user update for" qCDebug(LOG) << "On channel suspicious user update for"
<< payload.event.broadcasterUserLogin.c_str(); << payload.event.broadcasterUserLogin.qt();
} }
QString Connection::getSessionID() const QString Connection::getSessionID() const
@@ -637,6 +637,7 @@ MessagePtr makeAutomodHoldMessageBody(
displayName + ':', event.userLogin.qt(), MessageColor::Text, displayName + ':', event.userLogin.qt(), MessageColor::Text,
channel->getUserColor(event.userLogin.qt())); channel->getUserColor(event.userLogin.qt()));
// sender's message caught by AutoMod // sender's message caught by AutoMod
// XXX: add the structured message here
builder.emplace<TextElement>(event.message.text.qt(), builder.emplace<TextElement>(event.message.text.qt(),
MessageElementFlag::Text, MessageColor::Text); MessageElementFlag::Text, MessageColor::Text);
auto text = displayName % u": " % event.message.text.qt(); auto text = displayName % u": " % event.message.text.qt();
@@ -646,4 +647,113 @@ MessagePtr makeAutomodHoldMessageBody(
return builder.release(); 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<BadgeElement>(makeAutoModBadge(),
MessageElementFlag::BadgeChannelAuthority);
// Suspicious user header message
QString prefix = u"Suspicious User:"_s;
builder.emplace<TextElement>(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<TextElement>(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<TextElement>(u'#' + event.broadcasterUserLogin.qt(),
MessageElementFlag::ChannelName,
MessageColor::System)
->setLink({Link::JumpToChannel, event.broadcasterUserLogin.qt()});
builder.emplace<TimestampElement>(time.time());
builder.emplace<TwitchModerationElement>();
builder->loginName = event.userLogin.qt();
builder->flags.set(MessageFlag::PubSub, MessageFlag::LowTrustUsers);
// sender username
builder.emplace<MentionElement>(
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<TextElement>(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 } // namespace chatterino::eventsub
@@ -4,6 +4,7 @@
#include "providers/twitch/TwitchChannel.hpp" #include "providers/twitch/TwitchChannel.hpp"
#include "twitch-eventsub-ws/payloads/automod-message-hold-v2.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-moderate-v2.hpp"
#include "twitch-eventsub-ws/payloads/channel-suspicious-user-message-v1.hpp"
#include <QDateTime> #include <QDateTime>
@@ -156,4 +157,12 @@ MessagePtr makeAutomodHoldMessageBody(
TwitchChannel *channel, const QDateTime &time, TwitchChannel *channel, const QDateTime &time,
const lib::payload::automod_message_hold::v2::Event &event); 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 } // namespace chatterino::eventsub
@@ -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": [
]
}
@@ -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": [
]
}
@@ -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"
}
]
}
@@ -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"
}
]
}
+19
View File
@@ -82,6 +82,25 @@ const std::map<QString, std::string_view, QCompareCaseInsensitive>
"cost": 0 "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 class MockApplication : public mock::BaseApplication