From b01f50856d0c5c5cf1d4faab35e25ad36e97c44a Mon Sep 17 00:00:00 2001 From: pajlada Date: Sun, 9 Feb 2025 12:21:16 +0100 Subject: [PATCH] feat(eventsub): Add support for `channel.moderate` Warn (#5932) * fix(eventsub): Be less strict with strings & vectors String foo from `{"foo":null}` is now accepted, and returns an empty string std::vector foo from `{"foo":null}` is now accepted, and returns an empty array --- CHANGELOG.md | 2 +- .../ast/lib/templates/field-vector.tmpl | 18 ++--- .../ast/lib/templates/initializer-vector.tmpl | 2 +- .../payloads/channel-moderate-v2.hpp | 10 +-- .../payloads/channel-chat-message-v1.cpp | 66 +++++++++++-------- .../payloads/channel-chat-notification-v1.cpp | 66 +++++++++++-------- .../payloads/channel-moderate-v2.cpp | 52 ++++++++------- lib/twitch-eventsub-ws/src/string.cpp | 6 ++ src/providers/twitch/eventsub/Connection.cpp | 17 +++-- .../twitch/eventsub/MessageBuilder.cpp | 62 +++++++++++++++++ .../twitch/eventsub/MessageBuilder.hpp | 6 ++ 11 files changed, 211 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d836d47..e95268a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ - Bugfix: Fixed the reply button showing for inline whispers and announcements. (#5863) - Bugfix: Fixed suspicious user treatment update messages not being searchable. (#5865) - Bugfix: Ensure miniaudio backend exits even if it doesn't exit cleanly. (#5896) -- Dev: Add initial experimental EventSub support. (#5837, #5895, #5897, #5904, #5910, #5903, #5915, #5916, #5930, #5935) +- Dev: Add initial experimental EventSub support. (#5837, #5895, #5897, #5904, #5910, #5903, #5915, #5916, #5930, #5935, #5932) - Dev: Highlight checks now use non-capturing groups for the boundaries. (#5784) - Dev: Removed unused PubSub whisper code. (#5898) - Dev: Updated Conan dependencies. (#5776) diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/field-vector.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/field-vector.tmpl index 5bddb26a..e7d43585 100644 --- a/lib/twitch-eventsub-ws/ast/lib/templates/field-vector.tmpl +++ b/lib/twitch-eventsub-ws/ast/lib/templates/field-vector.tmpl @@ -1,14 +1,14 @@ {% if field.tag -%} static_assert(false && "JSON tag support is not implemented for vectors"); {%- endif %} +std::vector<{{field.type_name}}> v{{field.name}}; const auto *jv{{field.name}} = root.if_contains("{{field.json_name}}"); -if (jv{{field.name}} == nullptr) -{ - {% include 'error-missing-field.tmpl' indent content %} +if (jv{{field.name}} != nullptr && !jv{{field.name}}->is_null()) { + auto {{field.name}} = boost::json::try_value_to>(*jv{{field.name}}); + if ({{field.name}}.has_error()) + { + {% include 'error-failed-to-deserialize.tmpl' indent content %} + } else { + v{{field.name}} = std::move({{field.name}}.value()); + } } -auto {{field.name}} = boost::json::try_value_to>(*jv{{field.name}}); -if ({{field.name}}.has_error()) -{ - {% include 'error-failed-to-deserialize.tmpl' indent content %} -} - diff --git a/lib/twitch-eventsub-ws/ast/lib/templates/initializer-vector.tmpl b/lib/twitch-eventsub-ws/ast/lib/templates/initializer-vector.tmpl index bae60582..51d6e0af 100644 --- a/lib/twitch-eventsub-ws/ast/lib/templates/initializer-vector.tmpl +++ b/lib/twitch-eventsub-ws/ast/lib/templates/initializer-vector.tmpl @@ -1 +1 @@ -.{{field.name}} = std::move({{field.name}}.value()), +.{{field.name}} = std::move(v{{field.name}}), diff --git a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-moderate-v2.hpp b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-moderate-v2.hpp index 2f138ef1..98d0b47d 100644 --- a/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-moderate-v2.hpp +++ b/lib/twitch-eventsub-ws/include/twitch-eventsub-ws/payloads/channel-moderate-v2.hpp @@ -346,13 +346,13 @@ struct DenyUnbanRequest : public UnbanRequest { struct Warn { static constexpr std::string_view TAG = "warn"; - std::string userID; - std::string userLogin; - std::string userName; - std::string reason; + String userID; + String userLogin; + String userName; + String reason; // TODO: Verify we handle null for this as an empty vector - std::vector chatRulesCited; + std::vector chatRulesCited; }; struct Clear { diff --git a/lib/twitch-eventsub-ws/src/generated/payloads/channel-chat-message-v1.cpp b/lib/twitch-eventsub-ws/src/generated/payloads/channel-chat-message-v1.cpp index 4cf3eb2a..893db7fb 100644 --- a/lib/twitch-eventsub-ws/src/generated/payloads/channel-chat-message-v1.cpp +++ b/lib/twitch-eventsub-ws/src/generated/payloads/channel-chat-message-v1.cpp @@ -175,23 +175,27 @@ boost::json::result_for::type tag_invoke( return ownerID.error(); } + std::vector vformat; const auto *jvformat = root.if_contains("format"); - if (jvformat == nullptr) + if (jvformat != nullptr && !jvformat->is_null()) { - EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); - } - auto format = - boost::json::try_value_to>(*jvformat); - if (format.has_error()) - { - return format.error(); + auto format = + boost::json::try_value_to>(*jvformat); + if (format.has_error()) + { + return format.error(); + } + else + { + vformat = std::move(format.value()); + } } return Emote{ .id = std::move(id.value()), .emoteSetID = std::move(emoteSetID.value()), .ownerID = std::move(ownerID.value()), - .format = std::move(format.value()), + .format = std::move(vformat), }; } @@ -358,21 +362,26 @@ boost::json::result_for::type tag_invoke( return text.error(); } + std::vector vfragments; const auto *jvfragments = root.if_contains("fragments"); - if (jvfragments == nullptr) + if (jvfragments != nullptr && !jvfragments->is_null()) { - EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); - } - auto fragments = - boost::json::try_value_to>(*jvfragments); - if (fragments.has_error()) - { - return fragments.error(); + auto fragments = + boost::json::try_value_to>( + *jvfragments); + if (fragments.has_error()) + { + return fragments.error(); + } + else + { + vfragments = std::move(fragments.value()); + } } return Message{ .text = std::move(text.value()), - .fragments = std::move(fragments.value()), + .fragments = std::move(vfragments), }; } @@ -663,17 +672,20 @@ boost::json::result_for::type tag_invoke( return color.error(); } + std::vector vbadges; const auto *jvbadges = root.if_contains("badges"); - if (jvbadges == nullptr) + if (jvbadges != nullptr && !jvbadges->is_null()) { - EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); + auto badges = boost::json::try_value_to>(*jvbadges); + if (badges.has_error()) + { + return badges.error(); + } + else + { + vbadges = std::move(badges.value()); + } } - auto badges = boost::json::try_value_to>(*jvbadges); - if (badges.has_error()) - { - return badges.error(); - } - const auto *jvmessageID = root.if_contains("message_id"); if (jvmessageID == nullptr) { @@ -768,7 +780,7 @@ boost::json::result_for::type tag_invoke( .chatterUserLogin = std::move(chatterUserLogin.value()), .chatterUserName = std::move(chatterUserName.value()), .color = std::move(color.value()), - .badges = std::move(badges.value()), + .badges = std::move(vbadges), .messageID = std::move(messageID.value()), .messageType = std::move(messageType.value()), .message = std::move(message.value()), diff --git a/lib/twitch-eventsub-ws/src/generated/payloads/channel-chat-notification-v1.cpp b/lib/twitch-eventsub-ws/src/generated/payloads/channel-chat-notification-v1.cpp index 4391d4de..e0f5c70f 100644 --- a/lib/twitch-eventsub-ws/src/generated/payloads/channel-chat-notification-v1.cpp +++ b/lib/twitch-eventsub-ws/src/generated/payloads/channel-chat-notification-v1.cpp @@ -175,23 +175,27 @@ boost::json::result_for::type tag_invoke( return ownerID.error(); } + std::vector vformat; const auto *jvformat = root.if_contains("format"); - if (jvformat == nullptr) + if (jvformat != nullptr && !jvformat->is_null()) { - EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); - } - auto format = - boost::json::try_value_to>(*jvformat); - if (format.has_error()) - { - return format.error(); + auto format = + boost::json::try_value_to>(*jvformat); + if (format.has_error()) + { + return format.error(); + } + else + { + vformat = std::move(format.value()); + } } return Emote{ .id = std::move(id.value()), .emoteSetID = std::move(emoteSetID.value()), .ownerID = std::move(ownerID.value()), - .format = std::move(format.value()), + .format = std::move(vformat), }; } @@ -1246,21 +1250,26 @@ boost::json::result_for::type tag_invoke( return text.error(); } + std::vector vfragments; const auto *jvfragments = root.if_contains("fragments"); - if (jvfragments == nullptr) + if (jvfragments != nullptr && !jvfragments->is_null()) { - EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); - } - auto fragments = - boost::json::try_value_to>(*jvfragments); - if (fragments.has_error()) - { - return fragments.error(); + auto fragments = + boost::json::try_value_to>( + *jvfragments); + if (fragments.has_error()) + { + return fragments.error(); + } + else + { + vfragments = std::move(fragments.value()); + } } return Message{ .text = std::move(text.value()), - .fragments = std::move(fragments.value()), + .fragments = std::move(vfragments), }; } @@ -1389,17 +1398,20 @@ boost::json::result_for::type tag_invoke( return color.error(); } + std::vector vbadges; const auto *jvbadges = root.if_contains("badges"); - if (jvbadges == nullptr) + if (jvbadges != nullptr && !jvbadges->is_null()) { - EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); + auto badges = boost::json::try_value_to>(*jvbadges); + if (badges.has_error()) + { + return badges.error(); + } + else + { + vbadges = std::move(badges.value()); + } } - auto badges = boost::json::try_value_to>(*jvbadges); - if (badges.has_error()) - { - return badges.error(); - } - const auto *jvsystemMessage = root.if_contains("system_message"); if (jvsystemMessage == nullptr) { @@ -1649,7 +1661,7 @@ boost::json::result_for::type tag_invoke( .chatterUserName = std::move(chatterUserName.value()), .chatterIsAnonymous = chatterIsAnonymous.value(), .color = std::move(color.value()), - .badges = std::move(badges.value()), + .badges = std::move(vbadges), .systemMessage = std::move(systemMessage.value()), .messageID = std::move(messageID.value()), .message = std::move(message.value()), diff --git a/lib/twitch-eventsub-ws/src/generated/payloads/channel-moderate-v2.cpp b/lib/twitch-eventsub-ws/src/generated/payloads/channel-moderate-v2.cpp index daedf459..f3ecffc0 100644 --- a/lib/twitch-eventsub-ws/src/generated/payloads/channel-moderate-v2.cpp +++ b/lib/twitch-eventsub-ws/src/generated/payloads/channel-moderate-v2.cpp @@ -858,17 +858,21 @@ boost::json::result_for::type tag_invoke( return list.error(); } + std::vector vterms; const auto *jvterms = root.if_contains("terms"); - if (jvterms == nullptr) + if (jvterms != nullptr && !jvterms->is_null()) { - EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); + auto terms = + boost::json::try_value_to>(*jvterms); + if (terms.has_error()) + { + return terms.error(); + } + else + { + vterms = std::move(terms.value()); + } } - auto terms = boost::json::try_value_to>(*jvterms); - if (terms.has_error()) - { - return terms.error(); - } - static_assert(std::is_trivially_copyable_v().fromAutomod)>>); const auto *jvfromAutomod = root.if_contains("from_automod"); @@ -887,7 +891,7 @@ boost::json::result_for::type tag_invoke( return AutomodTerms{ .action = std::move(action.value()), .list = std::move(list.value()), - .terms = std::move(terms.value()), + .terms = std::move(vterms), .fromAutomod = fromAutomod.value(), }; } @@ -1037,7 +1041,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()) { @@ -1050,7 +1054,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()) { @@ -1063,7 +1067,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()) { @@ -1076,23 +1080,27 @@ boost::json::result_for::type tag_invoke( EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); } - auto reason = boost::json::try_value_to(*jvreason); + auto reason = boost::json::try_value_to(*jvreason); if (reason.has_error()) { return reason.error(); } + std::vector vchatRulesCited; const auto *jvchatRulesCited = root.if_contains("chat_rules_cited"); - if (jvchatRulesCited == nullptr) + if (jvchatRulesCited != nullptr && !jvchatRulesCited->is_null()) { - EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); - } - auto chatRulesCited = - boost::json::try_value_to>(*jvchatRulesCited); - if (chatRulesCited.has_error()) - { - return chatRulesCited.error(); + auto chatRulesCited = boost::json::try_value_to< + std::vector>(*jvchatRulesCited); + if (chatRulesCited.has_error()) + { + return chatRulesCited.error(); + } + else + { + vchatRulesCited = std::move(chatRulesCited.value()); + } } return Warn{ @@ -1100,7 +1108,7 @@ boost::json::result_for::type tag_invoke( .userLogin = std::move(userLogin.value()), .userName = std::move(userName.value()), .reason = std::move(reason.value()), - .chatRulesCited = std::move(chatRulesCited.value()), + .chatRulesCited = std::move(vchatRulesCited), }; } diff --git a/lib/twitch-eventsub-ws/src/string.cpp b/lib/twitch-eventsub-ws/src/string.cpp index 8d83ab80..c6b0312d 100644 --- a/lib/twitch-eventsub-ws/src/string.cpp +++ b/lib/twitch-eventsub-ws/src/string.cpp @@ -11,6 +11,12 @@ boost::json::result_for::type tag_invoke( boost::json::try_value_to_tag /*tag*/, const boost::json::value &jvRoot) { + if (jvRoot.is_null()) + { + // We treat null "strings" as empty strings + return String(""); + } + auto v = boost::json::try_value_to(jvRoot); if (v.has_error()) { diff --git a/src/providers/twitch/eventsub/Connection.cpp b/src/providers/twitch/eventsub/Connection.cpp index 04e27a60..8fed2dd4 100644 --- a/src/providers/twitch/eventsub/Connection.cpp +++ b/src/providers/twitch/eventsub/Connection.cpp @@ -164,12 +164,12 @@ void Connection::onChannelModerate( const auto now = QDateTime::currentDateTime(); std::visit( - [&](auto &&oAction) { - using Action = std::remove_cvref_t; + [&](auto &&action) { + using Action = std::remove_cvref_t; if constexpr (std::is_same_v< Action, lib::payload::channel_moderate::v2::Vip>) { - auto msg = makeVipMessage(channel, now, payload.event, oAction); + auto msg = makeVipMessage(channel, now, payload.event, action); runInGuiThread([channel, msg] { channel->addMessage(msg, MessageContext::Original); }); @@ -179,7 +179,16 @@ void Connection::onChannelModerate( lib::payload::channel_moderate::v2::Unvip>) { auto msg = - makeUnvipMessage(channel, now, payload.event, oAction); + makeUnvipMessage(channel, now, payload.event, action); + runInGuiThread([channel, msg] { + channel->addMessage(msg, MessageContext::Original); + }); + } + else if constexpr (std::is_same_v< + Action, + lib::payload::channel_moderate::v2::Warn>) + { + auto msg = makeWarnMessage(channel, now, payload.event, action); runInGuiThread([channel, msg] { channel->addMessage(msg, MessageContext::Original); }); diff --git a/src/providers/twitch/eventsub/MessageBuilder.cpp b/src/providers/twitch/eventsub/MessageBuilder.cpp index a6fd398e..b54c6ad3 100644 --- a/src/providers/twitch/eventsub/MessageBuilder.cpp +++ b/src/providers/twitch/eventsub/MessageBuilder.cpp @@ -78,4 +78,66 @@ MessagePtr makeUnvipMessage( return builder.release(); } +MessagePtr makeWarnMessage( + TwitchChannel *channel, const QDateTime &time, + const lib::payload::channel_moderate::v2::Event &event, + const lib::payload::channel_moderate::v2::Warn &action) +{ + MessageBuilder builder; + + QString text; + + builder.emplace(); + builder->flags.set(MessageFlag::System); + builder->flags.set(MessageFlag::Timeout); + builder->loginName = event.moderatorUserLogin.qt(); + + builder.emplace( + event.moderatorUserName.qt(), event.moderatorUserLogin.qt(), + MessageColor::System, + channel->getUserColor(event.moderatorUserLogin.qt())); + text.append(event.moderatorUserLogin.qt() + " "); + + builder.emplaceSystemTextAndUpdate("has warned", text); + + builder + .emplace(action.userName.qt(), action.userLogin.qt(), + MessageColor::System, + channel->getUserColor(action.userLogin.qt())) + ->setTrailingSpace(false); + text.append(action.userLogin.qt()); + + QStringList reasons; + + if (!action.reason.qt().isEmpty()) + { + reasons.append(action.reason.qt()); + } + + for (const auto &rule : action.chatRulesCited) + { + if (!rule.qt().isEmpty()) + { + reasons.append(rule.qt()); + } + } + + if (reasons.isEmpty()) + { + builder.emplaceSystemTextAndUpdate(".", text); + } + else + { + builder.emplaceSystemTextAndUpdate(":", text); + builder.emplaceSystemTextAndUpdate(reasons.join(", "), text); + } + + builder.message().messageText = text; + builder.message().searchText = text; + + builder.message().serverReceivedTime = time; + + return builder.release(); +} + } // namespace chatterino::eventsub diff --git a/src/providers/twitch/eventsub/MessageBuilder.hpp b/src/providers/twitch/eventsub/MessageBuilder.hpp index e122b936..a2db7ec2 100644 --- a/src/providers/twitch/eventsub/MessageBuilder.hpp +++ b/src/providers/twitch/eventsub/MessageBuilder.hpp @@ -20,4 +20,10 @@ MessagePtr makeUnvipMessage( const lib::payload::channel_moderate::v2::Event &event, const lib::payload::channel_moderate::v2::Unvip &action); +/// has warned : +MessagePtr makeWarnMessage( + TwitchChannel *channel, const QDateTime &time, + const lib::payload::channel_moderate::v2::Event &event, + const lib::payload::channel_moderate::v2::Warn &action); + } // namespace chatterino::eventsub