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<String> foo from `{"foo":null}` is now accepted, and returns an empty array
This commit is contained in:
pajlada
2025-02-09 12:21:16 +01:00
committed by GitHub
parent 88341517dc
commit b01f50856d
11 changed files with 211 additions and 96 deletions
+1 -1
View File
@@ -22,7 +22,7 @@
- Bugfix: Fixed the reply button showing for inline whispers and announcements. (#5863) - Bugfix: Fixed the reply button showing for inline whispers and announcements. (#5863)
- Bugfix: Fixed suspicious user treatment update messages not being searchable. (#5865) - Bugfix: Fixed suspicious user treatment update messages not being searchable. (#5865)
- Bugfix: Ensure miniaudio backend exits even if it doesn't exit cleanly. (#5896) - 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: 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)
- Dev: Updated Conan dependencies. (#5776) - Dev: Updated Conan dependencies. (#5776)
@@ -1,14 +1,14 @@
{% if field.tag -%} {% if field.tag -%}
static_assert(false && "JSON tag support is not implemented for vectors"); static_assert(false && "JSON tag support is not implemented for vectors");
{%- endif %} {%- endif %}
std::vector<{{field.type_name}}> v{{field.name}};
const auto *jv{{field.name}} = root.if_contains("{{field.json_name}}"); const auto *jv{{field.name}} = root.if_contains("{{field.json_name}}");
if (jv{{field.name}} == nullptr) if (jv{{field.name}} != nullptr && !jv{{field.name}}->is_null()) {
{ auto {{field.name}} = boost::json::try_value_to<std::vector<{{field.type_name}}>>(*jv{{field.name}});
{% include 'error-missing-field.tmpl' indent content %} 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<std::vector<{{field.type_name}}>>(*jv{{field.name}});
if ({{field.name}}.has_error())
{
{% include 'error-failed-to-deserialize.tmpl' indent content %}
}
@@ -1 +1 @@
.{{field.name}} = std::move({{field.name}}.value()), .{{field.name}} = std::move(v{{field.name}}),
@@ -346,13 +346,13 @@ struct DenyUnbanRequest : public UnbanRequest {
struct Warn { struct Warn {
static constexpr std::string_view TAG = "warn"; static constexpr std::string_view TAG = "warn";
std::string userID; String userID;
std::string userLogin; String userLogin;
std::string userName; String userName;
std::string reason; String reason;
// TODO: Verify we handle null for this as an empty vector // TODO: Verify we handle null for this as an empty vector
std::vector<std::string> chatRulesCited; std::vector<String> chatRulesCited;
}; };
struct Clear { struct Clear {
@@ -175,23 +175,27 @@ boost::json::result_for<Emote, boost::json::value>::type tag_invoke(
return ownerID.error(); return ownerID.error();
} }
std::vector<std::string> vformat;
const auto *jvformat = root.if_contains("format"); 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<std::vector<std::string>>(*jvformat);
auto format = if (format.has_error())
boost::json::try_value_to<std::vector<std::string>>(*jvformat); {
if (format.has_error()) return format.error();
{ }
return format.error(); else
{
vformat = std::move(format.value());
}
} }
return Emote{ return Emote{
.id = std::move(id.value()), .id = std::move(id.value()),
.emoteSetID = std::move(emoteSetID.value()), .emoteSetID = std::move(emoteSetID.value()),
.ownerID = std::move(ownerID.value()), .ownerID = std::move(ownerID.value()),
.format = std::move(format.value()), .format = std::move(vformat),
}; };
} }
@@ -358,21 +362,26 @@ boost::json::result_for<Message, boost::json::value>::type tag_invoke(
return text.error(); return text.error();
} }
std::vector<MessageFragment> vfragments;
const auto *jvfragments = root.if_contains("fragments"); 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<std::vector<MessageFragment>>(
auto fragments = *jvfragments);
boost::json::try_value_to<std::vector<MessageFragment>>(*jvfragments); if (fragments.has_error())
if (fragments.has_error()) {
{ return fragments.error();
return fragments.error(); }
else
{
vfragments = std::move(fragments.value());
}
} }
return Message{ return Message{
.text = std::move(text.value()), .text = std::move(text.value()),
.fragments = std::move(fragments.value()), .fragments = std::move(vfragments),
}; };
} }
@@ -663,17 +672,20 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
return color.error(); return color.error();
} }
std::vector<Badge> vbadges;
const auto *jvbadges = root.if_contains("badges"); 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<std::vector<Badge>>(*jvbadges);
if (badges.has_error())
{
return badges.error();
}
else
{
vbadges = std::move(badges.value());
}
} }
auto badges = boost::json::try_value_to<std::vector<Badge>>(*jvbadges);
if (badges.has_error())
{
return badges.error();
}
const auto *jvmessageID = root.if_contains("message_id"); const auto *jvmessageID = root.if_contains("message_id");
if (jvmessageID == nullptr) if (jvmessageID == nullptr)
{ {
@@ -768,7 +780,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
.chatterUserLogin = std::move(chatterUserLogin.value()), .chatterUserLogin = std::move(chatterUserLogin.value()),
.chatterUserName = std::move(chatterUserName.value()), .chatterUserName = std::move(chatterUserName.value()),
.color = std::move(color.value()), .color = std::move(color.value()),
.badges = std::move(badges.value()), .badges = std::move(vbadges),
.messageID = std::move(messageID.value()), .messageID = std::move(messageID.value()),
.messageType = std::move(messageType.value()), .messageType = std::move(messageType.value()),
.message = std::move(message.value()), .message = std::move(message.value()),
@@ -175,23 +175,27 @@ boost::json::result_for<Emote, boost::json::value>::type tag_invoke(
return ownerID.error(); return ownerID.error();
} }
std::vector<std::string> vformat;
const auto *jvformat = root.if_contains("format"); 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<std::vector<std::string>>(*jvformat);
auto format = if (format.has_error())
boost::json::try_value_to<std::vector<std::string>>(*jvformat); {
if (format.has_error()) return format.error();
{ }
return format.error(); else
{
vformat = std::move(format.value());
}
} }
return Emote{ return Emote{
.id = std::move(id.value()), .id = std::move(id.value()),
.emoteSetID = std::move(emoteSetID.value()), .emoteSetID = std::move(emoteSetID.value()),
.ownerID = std::move(ownerID.value()), .ownerID = std::move(ownerID.value()),
.format = std::move(format.value()), .format = std::move(vformat),
}; };
} }
@@ -1246,21 +1250,26 @@ boost::json::result_for<Message, boost::json::value>::type tag_invoke(
return text.error(); return text.error();
} }
std::vector<MessageFragment> vfragments;
const auto *jvfragments = root.if_contains("fragments"); 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<std::vector<MessageFragment>>(
auto fragments = *jvfragments);
boost::json::try_value_to<std::vector<MessageFragment>>(*jvfragments); if (fragments.has_error())
if (fragments.has_error()) {
{ return fragments.error();
return fragments.error(); }
else
{
vfragments = std::move(fragments.value());
}
} }
return Message{ return Message{
.text = std::move(text.value()), .text = std::move(text.value()),
.fragments = std::move(fragments.value()), .fragments = std::move(vfragments),
}; };
} }
@@ -1389,17 +1398,20 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
return color.error(); return color.error();
} }
std::vector<Badge> vbadges;
const auto *jvbadges = root.if_contains("badges"); 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<std::vector<Badge>>(*jvbadges);
if (badges.has_error())
{
return badges.error();
}
else
{
vbadges = std::move(badges.value());
}
} }
auto badges = boost::json::try_value_to<std::vector<Badge>>(*jvbadges);
if (badges.has_error())
{
return badges.error();
}
const auto *jvsystemMessage = root.if_contains("system_message"); const auto *jvsystemMessage = root.if_contains("system_message");
if (jvsystemMessage == nullptr) if (jvsystemMessage == nullptr)
{ {
@@ -1649,7 +1661,7 @@ boost::json::result_for<Event, boost::json::value>::type tag_invoke(
.chatterUserName = std::move(chatterUserName.value()), .chatterUserName = std::move(chatterUserName.value()),
.chatterIsAnonymous = chatterIsAnonymous.value(), .chatterIsAnonymous = chatterIsAnonymous.value(),
.color = std::move(color.value()), .color = std::move(color.value()),
.badges = std::move(badges.value()), .badges = std::move(vbadges),
.systemMessage = std::move(systemMessage.value()), .systemMessage = std::move(systemMessage.value()),
.messageID = std::move(messageID.value()), .messageID = std::move(messageID.value()),
.message = std::move(message.value()), .message = std::move(message.value()),
@@ -858,17 +858,21 @@ boost::json::result_for<AutomodTerms, boost::json::value>::type tag_invoke(
return list.error(); return list.error();
} }
std::vector<std::string> vterms;
const auto *jvterms = root.if_contains("terms"); 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<std::vector<std::string>>(*jvterms);
if (terms.has_error())
{
return terms.error();
}
else
{
vterms = std::move(terms.value());
}
} }
auto terms = boost::json::try_value_to<std::vector<std::string>>(*jvterms);
if (terms.has_error())
{
return terms.error();
}
static_assert(std::is_trivially_copyable_v<std::remove_reference_t< static_assert(std::is_trivially_copyable_v<std::remove_reference_t<
decltype(std::declval<AutomodTerms>().fromAutomod)>>); decltype(std::declval<AutomodTerms>().fromAutomod)>>);
const auto *jvfromAutomod = root.if_contains("from_automod"); const auto *jvfromAutomod = root.if_contains("from_automod");
@@ -887,7 +891,7 @@ boost::json::result_for<AutomodTerms, boost::json::value>::type tag_invoke(
return AutomodTerms{ return AutomodTerms{
.action = std::move(action.value()), .action = std::move(action.value()),
.list = std::move(list.value()), .list = std::move(list.value()),
.terms = std::move(terms.value()), .terms = std::move(vterms),
.fromAutomod = fromAutomod.value(), .fromAutomod = fromAutomod.value(),
}; };
} }
@@ -1037,7 +1041,7 @@ boost::json::result_for<Warn, 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())
{ {
@@ -1050,7 +1054,7 @@ boost::json::result_for<Warn, 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())
{ {
@@ -1063,7 +1067,7 @@ boost::json::result_for<Warn, 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())
{ {
@@ -1076,23 +1080,27 @@ boost::json::result_for<Warn, boost::json::value>::type tag_invoke(
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing); EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
} }
auto reason = boost::json::try_value_to<std::string>(*jvreason); auto reason = boost::json::try_value_to<String>(*jvreason);
if (reason.has_error()) if (reason.has_error())
{ {
return reason.error(); return reason.error();
} }
std::vector<chatterino::eventsub::lib::String> vchatRulesCited;
const auto *jvchatRulesCited = root.if_contains("chat_rules_cited"); 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<
} std::vector<chatterino::eventsub::lib::String>>(*jvchatRulesCited);
auto chatRulesCited = if (chatRulesCited.has_error())
boost::json::try_value_to<std::vector<std::string>>(*jvchatRulesCited); {
if (chatRulesCited.has_error()) return chatRulesCited.error();
{ }
return chatRulesCited.error(); else
{
vchatRulesCited = std::move(chatRulesCited.value());
}
} }
return Warn{ return Warn{
@@ -1100,7 +1108,7 @@ boost::json::result_for<Warn, boost::json::value>::type tag_invoke(
.userLogin = std::move(userLogin.value()), .userLogin = std::move(userLogin.value()),
.userName = std::move(userName.value()), .userName = std::move(userName.value()),
.reason = std::move(reason.value()), .reason = std::move(reason.value()),
.chatRulesCited = std::move(chatRulesCited.value()), .chatRulesCited = std::move(vchatRulesCited),
}; };
} }
+6
View File
@@ -11,6 +11,12 @@ boost::json::result_for<String, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<String> /*tag*/, boost::json::try_value_to_tag<String> /*tag*/,
const boost::json::value &jvRoot) 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<std::string>(jvRoot); auto v = boost::json::try_value_to<std::string>(jvRoot);
if (v.has_error()) if (v.has_error())
{ {
+13 -4
View File
@@ -164,12 +164,12 @@ void Connection::onChannelModerate(
const auto now = QDateTime::currentDateTime(); const auto now = QDateTime::currentDateTime();
std::visit( std::visit(
[&](auto &&oAction) { [&](auto &&action) {
using Action = std::remove_cvref_t<decltype(oAction)>; using Action = std::remove_cvref_t<decltype(action)>;
if constexpr (std::is_same_v< if constexpr (std::is_same_v<
Action, lib::payload::channel_moderate::v2::Vip>) 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] { runInGuiThread([channel, msg] {
channel->addMessage(msg, MessageContext::Original); channel->addMessage(msg, MessageContext::Original);
}); });
@@ -179,7 +179,16 @@ void Connection::onChannelModerate(
lib::payload::channel_moderate::v2::Unvip>) lib::payload::channel_moderate::v2::Unvip>)
{ {
auto msg = 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] { runInGuiThread([channel, msg] {
channel->addMessage(msg, MessageContext::Original); channel->addMessage(msg, MessageContext::Original);
}); });
@@ -78,4 +78,66 @@ MessagePtr makeUnvipMessage(
return builder.release(); 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<TimestampElement>();
builder->flags.set(MessageFlag::System);
builder->flags.set(MessageFlag::Timeout);
builder->loginName = event.moderatorUserLogin.qt();
builder.emplace<MentionElement>(
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<MentionElement>(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 } // namespace chatterino::eventsub
@@ -20,4 +20,10 @@ MessagePtr makeUnvipMessage(
const lib::payload::channel_moderate::v2::Event &event, const lib::payload::channel_moderate::v2::Event &event,
const lib::payload::channel_moderate::v2::Unvip &action); const lib::payload::channel_moderate::v2::Unvip &action);
/// <MODERATOR> has warned <USER>: <REASON>
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 } // namespace chatterino::eventsub