feat(eventsub): implement channel.chat.user_message_(hold/update) (#6008)

This commit is contained in:
pajlada
2025-03-01 17:00:40 +01:00
committed by GitHub
parent f4541b0208
commit c03b883f05
22 changed files with 1123 additions and 1 deletions
+1 -1
View File
@@ -31,7 +31,7 @@
- Bugfix: Fixed color input thinking blue is also red. (#5982)
- Bugfix: Fixed an issue where commands would sometimes reset if Chatterino was improperly shut down. (#6011)
- 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, #6007, #6010)
- 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, #6010, #6008)
- 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)
@@ -145,6 +145,24 @@ public:
benchmark::DoNotOptimize(&metadata);
benchmark::DoNotOptimize(&payload);
}
void onChannelChatUserMessageHold(
const messages::Metadata &metadata,
const payload::channel_chat_user_message_hold::v1::Payload &payload)
override
{
benchmark::DoNotOptimize(&metadata);
benchmark::DoNotOptimize(&payload);
}
void onChannelChatUserMessageUpdate(
const messages::Metadata &metadata,
const payload::channel_chat_user_message_update::v1::Payload &payload)
override
{
benchmark::DoNotOptimize(&metadata);
benchmark::DoNotOptimize(&payload);
}
// NOLINTEND(cppcoreguidelines-pro-type-const-cast)
};
@@ -6,6 +6,8 @@
#include "twitch-eventsub-ws/payloads/channel-ban-v1.hpp"
#include "twitch-eventsub-ws/payloads/channel-chat-message-v1.hpp"
#include "twitch-eventsub-ws/payloads/channel-chat-notification-v1.hpp"
#include "twitch-eventsub-ws/payloads/channel-chat-user-message-hold-v1.hpp"
#include "twitch-eventsub-ws/payloads/channel-chat-user-message-update-v1.hpp"
#include "twitch-eventsub-ws/payloads/channel-moderate-v2.hpp"
#include "twitch-eventsub-ws/payloads/channel-suspicious-user-message-v1.hpp"
#include "twitch-eventsub-ws/payloads/channel-suspicious-user-update-v1.hpp"
@@ -74,6 +76,16 @@ public:
const messages::Metadata &metadata,
const payload::channel_suspicious_user_update::v1::Payload
&payload) = 0;
virtual void onChannelChatUserMessageHold(
const messages::Metadata &metadata,
const payload::channel_chat_user_message_hold::v1::Payload
&payload) = 0;
virtual void onChannelChatUserMessageUpdate(
const messages::Metadata &metadata,
const payload::channel_chat_user_message_update::v1::Payload
&payload) = 0;
// Add your new subscription types above this line
};
@@ -0,0 +1,37 @@
#pragma once
#include "twitch-eventsub-ws/payloads/structured-message.hpp"
#include "twitch-eventsub-ws/payloads/subscription.hpp"
#include "twitch-eventsub-ws/string.hpp"
#include <boost/json.hpp>
namespace chatterino::eventsub::lib::payload::channel_chat_user_message_hold::
v1 {
// {"subscription":{"id":"94ab19f1-dd41-41e7-a5bc-f8f45bd4342a","status":"enabled","type":"channel.chat.user_message_hold","version":"1","condition":{"broadcaster_user_id":"117166826","user_id":"159849156"},"transport":{"method":"websocket","session_id":"AgoQP515xn0yRxGvyU29dY9grBIGY2VsbC1j"},"created_at":"2025-03-01T10:40:14.527982426Z","cost":0},"event":{"broadcaster_user_id":"117166826","broadcaster_user_login":"testaccount_420","broadcaster_user_name":"테스트계정420","user_id":"159849156","user_login":"bajlada","user_name":"BajLada","message_id":"e39e3c58-3d25-49fd-8c94-e776ef57a7f8","message":{"text":"penis penis penis","fragments":[{"type":"text","text":"penis penis penis","cheermote":null,"emote":null}]}}}
struct Event {
// Broadcaster of the channel the message was sent in
String broadcasterUserID;
String broadcasterUserLogin;
String broadcasterUserName;
// User who sent the message
String userID;
String userLogin;
String userName;
String messageID;
chat::Message message;
};
struct Payload {
subscription::Subscription subscription;
Event event;
};
#include "twitch-eventsub-ws/payloads/channel-chat-user-message-hold-v1.inc"
} // namespace chatterino::eventsub::lib::payload::channel_chat_user_message_hold::v1
@@ -0,0 +1,5 @@
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot);
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot);
@@ -0,0 +1,49 @@
#pragma once
#include "twitch-eventsub-ws/payloads/structured-message.hpp"
#include "twitch-eventsub-ws/payloads/subscription.hpp"
#include "twitch-eventsub-ws/string.hpp"
#include <boost/json.hpp>
namespace chatterino::eventsub::lib::payload::channel_chat_user_message_update::
v1 {
// message approved:
// {"subscription":{"id":"3a9fd3ec-f5b5-49d7-8624-2195b3ebfda9","status":"enabled","type":"channel.chat.user_message_update","version":"1","condition":{"broadcaster_user_id":"117166826","user_id":"159849156"},"transport":{"method":"websocket","session_id":"AgoQBFnyoXrLQxiCmkHTvJ8VNBIGY2VsbC1j"},"created_at":"2025-03-01T11:29:49.532375428Z","cost":0},"event":{"broadcaster_user_id":"117166826","broadcaster_user_login":"testaccount_420","broadcaster_user_name":"테스트계정420","user_id":"159849156","user_login":"bajlada","user_name":"BajLada","status":"approved","message_id":"5686ec52-e02c-4042-ac21-1dfd8cab0f9f","message":{"text":"penis ass penis","fragments":[{"type":"text","text":"penis ass penis","cheermote":null,"emote":null}]}}}
// message denied:
// {"subscription":{"id":"3a9fd3ec-f5b5-49d7-8624-2195b3ebfda9","status":"enabled","type":"channel.chat.user_message_update","version":"1","condition":{"broadcaster_user_id":"117166826","user_id":"159849156"},"transport":{"method":"websocket","session_id":"AgoQBFnyoXrLQxiCmkHTvJ8VNBIGY2VsbC1j"},"created_at":"2025-03-01T11:29:49.532375428Z","cost":0},"event":{"broadcaster_user_id":"117166826","broadcaster_user_login":"testaccount_420","broadcaster_user_name":"테스트계정420","user_id":"159849156","user_login":"bajlada","user_name":"BajLada","status":"denied","message_id":"fc90dc3b-0634-41c3-8365-f40719f076ab","message":{"text":"penis ass penis","fragments":[{"type":"text","text":"penis ","cheermote":null,"emote":null},{"type":"text","text":"ass penis","cheermote":null,"emote":null}]}}}
enum class Status : std::uint8_t {
Approved,
Denied,
Invalid,
};
struct Event {
// Broadcaster of the channel the message was sent in
String broadcasterUserID;
String broadcasterUserLogin;
String broadcasterUserName;
// User who sent the message
String userID;
String userLogin;
String userName;
Status status;
String messageID;
chat::Message message;
};
struct Payload {
subscription::Subscription subscription;
Event event;
};
#include "twitch-eventsub-ws/payloads/channel-chat-user-message-update-v1.inc"
} // namespace chatterino::eventsub::lib::payload::channel_chat_user_message_update::v1
@@ -0,0 +1,8 @@
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<Event, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Event>, const boost::json::value &jvRoot);
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Payload>, const boost::json::value &jvRoot);
@@ -12,6 +12,8 @@ generate_json_impls(
include/twitch-eventsub-ws/payloads/channel-ban-v1.hpp
include/twitch-eventsub-ws/payloads/channel-chat-message-v1.hpp
include/twitch-eventsub-ws/payloads/channel-chat-notification-v1.hpp
include/twitch-eventsub-ws/payloads/channel-chat-user-message-hold-v1.hpp
include/twitch-eventsub-ws/payloads/channel-chat-user-message-update-v1.hpp
include/twitch-eventsub-ws/payloads/channel-moderate-v2.hpp
include/twitch-eventsub-ws/payloads/channel-suspicious-user-message-v1.hpp
include/twitch-eventsub-ws/payloads/channel-suspicious-user-update-v1.hpp
@@ -0,0 +1,186 @@
// 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/channel-chat-user-message-hold-v1.hpp"
#include <boost/json.hpp>
namespace chatterino::eventsub::lib::payload::channel_chat_user_message_hold::
v1 {
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Event> /* tag */,
const boost::json::value &jvRoot)
{
if (!jvRoot.is_object())
{
EVENTSUB_BAIL_HERE(error::Kind::ExpectedObject);
}
const auto &root = jvRoot.get_object();
const auto *jvbroadcasterUserID = root.if_contains("broadcaster_user_id");
if (jvbroadcasterUserID == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto broadcasterUserID =
boost::json::try_value_to<String>(*jvbroadcasterUserID);
if (broadcasterUserID.has_error())
{
return broadcasterUserID.error();
}
const auto *jvbroadcasterUserLogin =
root.if_contains("broadcaster_user_login");
if (jvbroadcasterUserLogin == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto broadcasterUserLogin =
boost::json::try_value_to<String>(*jvbroadcasterUserLogin);
if (broadcasterUserLogin.has_error())
{
return broadcasterUserLogin.error();
}
const auto *jvbroadcasterUserName =
root.if_contains("broadcaster_user_name");
if (jvbroadcasterUserName == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto broadcasterUserName =
boost::json::try_value_to<String>(*jvbroadcasterUserName);
if (broadcasterUserName.has_error())
{
return broadcasterUserName.error();
}
const auto *jvuserID = root.if_contains("user_id");
if (jvuserID == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto userID = boost::json::try_value_to<String>(*jvuserID);
if (userID.has_error())
{
return userID.error();
}
const auto *jvuserLogin = root.if_contains("user_login");
if (jvuserLogin == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto userLogin = boost::json::try_value_to<String>(*jvuserLogin);
if (userLogin.has_error())
{
return userLogin.error();
}
const auto *jvuserName = root.if_contains("user_name");
if (jvuserName == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto userName = boost::json::try_value_to<String>(*jvuserName);
if (userName.has_error())
{
return userName.error();
}
const auto *jvmessageID = root.if_contains("message_id");
if (jvmessageID == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto messageID = boost::json::try_value_to<String>(*jvmessageID);
if (messageID.has_error())
{
return messageID.error();
}
const auto *jvmessage = root.if_contains("message");
if (jvmessage == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto message = boost::json::try_value_to<chat::Message>(*jvmessage);
if (message.has_error())
{
return message.error();
}
return Event{
.broadcasterUserID = std::move(broadcasterUserID.value()),
.broadcasterUserLogin = std::move(broadcasterUserLogin.value()),
.broadcasterUserName = std::move(broadcasterUserName.value()),
.userID = std::move(userID.value()),
.userLogin = std::move(userLogin.value()),
.userName = std::move(userName.value()),
.messageID = std::move(messageID.value()),
.message = std::move(message.value()),
};
}
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Payload> /* tag */,
const boost::json::value &jvRoot)
{
if (!jvRoot.is_object())
{
EVENTSUB_BAIL_HERE(error::Kind::ExpectedObject);
}
const auto &root = jvRoot.get_object();
const auto *jvsubscription = root.if_contains("subscription");
if (jvsubscription == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto subscription =
boost::json::try_value_to<subscription::Subscription>(*jvsubscription);
if (subscription.has_error())
{
return subscription.error();
}
const auto *jvevent = root.if_contains("event");
if (jvevent == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto event = boost::json::try_value_to<Event>(*jvevent);
if (event.has_error())
{
return event.error();
}
return Payload{
.subscription = std::move(subscription.value()),
.event = std::move(event.value()),
};
}
} // namespace chatterino::eventsub::lib::payload::channel_chat_user_message_hold::v1
@@ -0,0 +1,229 @@
// 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/channel-chat-user-message-update-v1.hpp"
#include <boost/json.hpp>
namespace chatterino::eventsub::lib::payload::channel_chat_user_message_update::
v1 {
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 == "approved"sv)
{
return Status::Approved;
}
if (eString == "denied"sv)
{
return Status::Denied;
}
if (eString == "invalid"sv)
{
return Status::Invalid;
}
EVENTSUB_BAIL_HERE(error::Kind::UnknownEnumValue);
}
boost::json::result_for<Event, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Event> /* tag */,
const boost::json::value &jvRoot)
{
if (!jvRoot.is_object())
{
EVENTSUB_BAIL_HERE(error::Kind::ExpectedObject);
}
const auto &root = jvRoot.get_object();
const auto *jvbroadcasterUserID = root.if_contains("broadcaster_user_id");
if (jvbroadcasterUserID == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto broadcasterUserID =
boost::json::try_value_to<String>(*jvbroadcasterUserID);
if (broadcasterUserID.has_error())
{
return broadcasterUserID.error();
}
const auto *jvbroadcasterUserLogin =
root.if_contains("broadcaster_user_login");
if (jvbroadcasterUserLogin == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto broadcasterUserLogin =
boost::json::try_value_to<String>(*jvbroadcasterUserLogin);
if (broadcasterUserLogin.has_error())
{
return broadcasterUserLogin.error();
}
const auto *jvbroadcasterUserName =
root.if_contains("broadcaster_user_name");
if (jvbroadcasterUserName == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto broadcasterUserName =
boost::json::try_value_to<String>(*jvbroadcasterUserName);
if (broadcasterUserName.has_error())
{
return broadcasterUserName.error();
}
const auto *jvuserID = root.if_contains("user_id");
if (jvuserID == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto userID = boost::json::try_value_to<String>(*jvuserID);
if (userID.has_error())
{
return userID.error();
}
const auto *jvuserLogin = root.if_contains("user_login");
if (jvuserLogin == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto userLogin = boost::json::try_value_to<String>(*jvuserLogin);
if (userLogin.has_error())
{
return userLogin.error();
}
const auto *jvuserName = root.if_contains("user_name");
if (jvuserName == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto userName = boost::json::try_value_to<String>(*jvuserName);
if (userName.has_error())
{
return userName.error();
}
static_assert(
std::is_trivially_copyable_v<
std::remove_reference_t<decltype(std::declval<Event>().status)>>);
const auto *jvstatus = root.if_contains("status");
if (jvstatus == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto status = boost::json::try_value_to<Status>(*jvstatus);
if (status.has_error())
{
return status.error();
}
const auto *jvmessageID = root.if_contains("message_id");
if (jvmessageID == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto messageID = boost::json::try_value_to<String>(*jvmessageID);
if (messageID.has_error())
{
return messageID.error();
}
const auto *jvmessage = root.if_contains("message");
if (jvmessage == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto message = boost::json::try_value_to<chat::Message>(*jvmessage);
if (message.has_error())
{
return message.error();
}
return Event{
.broadcasterUserID = std::move(broadcasterUserID.value()),
.broadcasterUserLogin = std::move(broadcasterUserLogin.value()),
.broadcasterUserName = std::move(broadcasterUserName.value()),
.userID = std::move(userID.value()),
.userLogin = std::move(userLogin.value()),
.userName = std::move(userName.value()),
.status = status.value(),
.messageID = std::move(messageID.value()),
.message = std::move(message.value()),
};
}
boost::json::result_for<Payload, boost::json::value>::type tag_invoke(
boost::json::try_value_to_tag<Payload> /* tag */,
const boost::json::value &jvRoot)
{
if (!jvRoot.is_object())
{
EVENTSUB_BAIL_HERE(error::Kind::ExpectedObject);
}
const auto &root = jvRoot.get_object();
const auto *jvsubscription = root.if_contains("subscription");
if (jvsubscription == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto subscription =
boost::json::try_value_to<subscription::Subscription>(*jvsubscription);
if (subscription.has_error())
{
return subscription.error();
}
const auto *jvevent = root.if_contains("event");
if (jvevent == nullptr)
{
EVENTSUB_BAIL_HERE(error::Kind::FieldMissing);
}
auto event = boost::json::try_value_to<Event>(*jvevent);
if (event.has_error())
{
return event.error();
}
return Payload{
.subscription = std::move(subscription.value()),
.event = std::move(event.value()),
};
}
} // namespace chatterino::eventsub::lib::payload::channel_chat_user_message_update::v1
+26
View File
@@ -212,6 +212,32 @@ namespace {
return boost::system::error_code{};
},
},
{
{"channel.chat.user_message_hold", "1"},
[](const auto &metadata, const auto &jv, auto &listener) {
auto oPayload = parsePayload<
payload::channel_chat_user_message_hold::v1::Payload>(jv);
if (!oPayload)
{
return oPayload.error();
}
listener->onChannelChatUserMessageHold(metadata, *oPayload);
return boost::system::error_code{};
},
},
{
{"channel.chat.user_message_update", "1"},
[](const auto &metadata, const auto &jv, auto &listener) {
auto oPayload = parsePayload<
payload::channel_chat_user_message_update::v1::Payload>(jv);
if (!oPayload)
{
return oPayload.error();
}
listener->onChannelChatUserMessageUpdate(metadata, *oPayload);
return boost::system::error_code{};
},
},
// Add your new subscription types above this line
};
@@ -129,6 +129,18 @@ class NoOpListener : public chatterino::eventsub::lib::Listener
override
{
}
void onChannelChatUserMessageHold(
const messages::Metadata &metadata,
const payload::channel_chat_user_message_hold::v1::Payload &payload)
override
{
}
void onChannelChatUserMessageUpdate(
const messages::Metadata &metadata,
const payload::channel_chat_user_message_update::v1::Payload &payload)
override
{
}
};
} // namespace
+38
View File
@@ -1554,6 +1554,9 @@ void TwitchChannel::refreshPubSub()
},
},
});
this->eventSubChannelChatUserMessageHoldHandle.reset();
this->eventSubChannelChatUserMessageUpdateHandle.reset();
}
else
{
@@ -1561,7 +1564,42 @@ void TwitchChannel::refreshPubSub()
this->eventSubAutomodMessageHoldHandle.reset();
this->eventSubAutomodMessageUpdateHandle.reset();
this->eventSubSuspiciousUserMessageHandle.reset();
this->eventSubChannelChatUserMessageHoldHandle =
getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{
.subscriptionType = "channel.chat.user_message_hold",
.subscriptionVersion = "1",
.conditions =
{
{
"broadcaster_user_id",
roomId,
},
{
"user_id",
currentAccount->getUserId(),
},
},
});
this->eventSubChannelChatUserMessageUpdateHandle =
getApp()->getEventSub()->subscribe(eventsub::SubscriptionRequest{
.subscriptionType = "channel.chat.user_message_update",
.subscriptionVersion = "1",
.conditions =
{
{
"broadcaster_user_id",
roomId,
},
{
"user_id",
currentAccount->getUserId(),
},
},
});
}
getApp()->getTwitchPubSub()->listenToChannelPointRewards(roomId);
}
+2
View File
@@ -512,6 +512,8 @@ private:
eventsub::SubscriptionHandle eventSubAutomodMessageHoldHandle;
eventsub::SubscriptionHandle eventSubAutomodMessageUpdateHandle;
eventsub::SubscriptionHandle eventSubSuspiciousUserMessageHandle;
eventsub::SubscriptionHandle eventSubChannelChatUserMessageHoldHandle;
eventsub::SubscriptionHandle eventSubChannelChatUserMessageUpdateHandle;
friend class TwitchIrcServer;
friend class MessageBuilder;
@@ -305,6 +305,57 @@ void Connection::onChannelSuspiciousUserUpdate(
<< payload.event.broadcasterUserLogin.qt();
}
void Connection::onChannelChatUserMessageHold(
const lib::messages::Metadata &metadata,
const lib::payload::channel_chat_user_message_hold::v1::Payload &payload)
{
auto *channel = dynamic_cast<TwitchChannel *>(
getApp()
->getTwitch()
->getChannelOrEmpty(payload.event.broadcasterUserLogin.qt())
.get());
if (!channel || channel->isEmpty())
{
qCDebug(LOG) << "Channel Chat User Message Hold for broadcaster we're "
"not interested in"
<< payload.event.broadcasterUserLogin.qt();
return;
}
auto time = chronoToQDateTime(metadata.messageTimestamp);
auto message = makeUserMessageHeldMessage(channel, time, payload.event);
runInGuiThread([channel, message] {
channel->addMessage(message, MessageContext::Original);
});
}
void Connection::onChannelChatUserMessageUpdate(
const lib::messages::Metadata &metadata,
const lib::payload::channel_chat_user_message_update::v1::Payload &payload)
{
auto *channel = dynamic_cast<TwitchChannel *>(
getApp()
->getTwitch()
->getChannelOrEmpty(payload.event.broadcasterUserLogin.qt())
.get());
if (!channel || channel->isEmpty())
{
qCDebug(LOG)
<< "Channel Chat User Message Update for broadcaster we're "
"not interested in"
<< payload.event.broadcasterUserLogin.qt();
return;
}
auto time = chronoToQDateTime(metadata.messageTimestamp);
auto message = makeUserMessageUpdateMessage(channel, time, payload.event);
runInGuiThread([channel, message] {
channel->addMessage(message, MessageContext::Original);
});
}
QString Connection::getSessionID() const
{
return this->sessionID;
@@ -71,6 +71,16 @@ public:
const lib::payload::channel_suspicious_user_update::v1::Payload
&payload) override;
void onChannelChatUserMessageHold(
const lib::messages::Metadata &metadata,
const lib::payload::channel_chat_user_message_hold::v1::Payload
&payload) override;
void onChannelChatUserMessageUpdate(
const lib::messages::Metadata &metadata,
const lib::payload::channel_chat_user_message_update::v1::Payload
&payload) override;
QString getSessionID() const;
bool isSubscribedTo(const SubscriptionRequest &request) const;
@@ -756,4 +756,86 @@ MessagePtr makeSuspiciousUserMessageBody(
return builder.release();
}
MessagePtr makeUserMessageHeldMessage(
TwitchChannel *channel, const QDateTime &time,
const lib::payload::channel_chat_user_message_hold::v1::Event &event)
{
QString text("AutoMod: Hey! Your message is being checked by mods and has "
"not been sent.");
EventSubMessageBuilder builder(channel);
builder->serverReceivedTime = time;
builder->id = u"automod_" % event.messageID.qt();
builder->loginName = u"automod"_s;
builder->channelName = event.broadcasterUserLogin.qt();
builder->flags.set(MessageFlag::PubSub, MessageFlag::Timeout,
MessageFlag::AutoMod);
// AutoMod shield badge
builder.emplace<BadgeElement>(makeAutoModBadge(),
MessageElementFlag::BadgeChannelAuthority);
// AutoMod "username"
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::Text,
QColor(0, 0, 255), FontStyle::ChatMediumBold);
builder.emplace<TextElement>(
"Hey! Your message is being checked by mods and has not been sent.",
MessageElementFlag::Text, MessageColor::Text);
builder->messageText = text;
builder->searchText = text;
return builder.release();
}
MessagePtr makeUserMessageUpdateMessage(
TwitchChannel *channel, const QDateTime &time,
const lib::payload::channel_chat_user_message_update::v1::Event &event)
{
using lib::payload::channel_chat_user_message_update::v1::Status;
QString text("AutoMod: ");
EventSubMessageBuilder builder(channel);
builder->serverReceivedTime = time;
builder->id = u"automod_" % event.messageID.qt();
builder->loginName = u"automod"_s;
builder->channelName = event.broadcasterUserLogin.qt();
builder->flags.set(MessageFlag::PubSub, MessageFlag::Timeout,
MessageFlag::AutoMod);
// AutoMod shield badge
builder.emplace<BadgeElement>(makeAutoModBadge(),
MessageElementFlag::BadgeChannelAuthority);
// AutoMod "username"
builder.emplace<TextElement>("AutoMod:", MessageElementFlag::Text,
QColor(0, 0, 255), FontStyle::ChatMediumBold);
switch (event.status)
{
case Status::Approved:
text += "Mods have accepted your message.";
builder.emplace<TextElement>("Mods have accepted your message.",
MessageElementFlag::Text,
MessageColor::Text);
break;
case Status::Denied:
text += "Mods have denied your message.";
builder.emplace<TextElement>("Mods have denied your message.",
MessageElementFlag::Text,
MessageColor::Text);
break;
case Status::Invalid:
text += "Your message was lost in the void.";
builder.emplace<TextElement>("Your message was lost in the void.",
MessageElementFlag::Text,
MessageColor::Text);
break;
}
builder->messageText = text;
builder->searchText = text;
return builder.release();
}
} // namespace chatterino::eventsub
@@ -3,6 +3,8 @@
#include "messages/MessageBuilder.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "twitch-eventsub-ws/payloads/automod-message-hold-v2.hpp"
#include "twitch-eventsub-ws/payloads/channel-chat-user-message-hold-v1.hpp"
#include "twitch-eventsub-ws/payloads/channel-chat-user-message-update-v1.hpp"
#include "twitch-eventsub-ws/payloads/channel-moderate-v2.hpp"
#include "twitch-eventsub-ws/payloads/channel-suspicious-user-message-v1.hpp"
@@ -165,4 +167,12 @@ MessagePtr makeSuspiciousUserMessageBody(
TwitchChannel *channel, const QDateTime &time,
const lib::payload::channel_suspicious_user_message::v1::Event &event);
MessagePtr makeUserMessageHeldMessage(
TwitchChannel *channel, const QDateTime &time,
const lib::payload::channel_chat_user_message_hold::v1::Event &event);
MessagePtr makeUserMessageUpdateMessage(
TwitchChannel *channel, const QDateTime &time,
const lib::payload::channel_chat_user_message_update::v1::Event &event);
} // namespace chatterino::eventsub
@@ -0,0 +1,105 @@
{
"input": {
"broadcaster_user_id": "11148817",
"broadcaster_user_login": "pajlada",
"broadcaster_user_name": "pajlada",
"message": {
"fragments": [
{
"cheermote": null,
"emote": null,
"text": "penis penis penis",
"type": "text"
}
],
"text": "penis penis penis"
},
"message_id": "e39e3c58-3d25-49fd-8c94-e776ef57a7f8",
"user_id": "159849156",
"user_login": "bajlada",
"user_name": "BajLada"
},
"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": [
"AutoMod:"
]
},
{
"color": "Text",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"Hey!",
"Your",
"message",
"is",
"being",
"checked",
"by",
"mods",
"and",
"has",
"not",
"been",
"sent."
]
}
],
"flags": "Timeout|PubSub|AutoMod|EventSub",
"id": "automod_e39e3c58-3d25-49fd-8c94-e776ef57a7f8",
"localizedName": "",
"loginName": "automod",
"messageText": "AutoMod: Hey! Your message is being checked by mods and has not been sent.",
"searchText": "AutoMod: Hey! Your message is being checked by mods and has not been sent.",
"serverReceivedTime": "2024-05-14T12:31:47Z",
"timeoutUser": "",
"userID": "",
"usernameColor": "#ff000000"
}
]
}
@@ -0,0 +1,98 @@
{
"input": {
"broadcaster_user_id": "11148817",
"broadcaster_user_login": "pajlada",
"broadcaster_user_name": "pajlada",
"message": {
"fragments": [
{
"cheermote": null,
"emote": null,
"text": "penis ass penis",
"type": "text"
}
],
"text": "penis ass penis"
},
"message_id": "5686ec52-e02c-4042-ac21-1dfd8cab0f9f",
"status": "approved",
"user_id": "159849156",
"user_login": "bajlada",
"user_name": "BajLada"
},
"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": [
"AutoMod:"
]
},
{
"color": "Text",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"Mods",
"have",
"accepted",
"your",
"message."
]
}
],
"flags": "Timeout|PubSub|AutoMod|EventSub",
"id": "automod_5686ec52-e02c-4042-ac21-1dfd8cab0f9f",
"localizedName": "",
"loginName": "automod",
"messageText": "AutoMod: Mods have accepted your message.",
"searchText": "AutoMod: Mods have accepted your message.",
"serverReceivedTime": "2024-05-14T12:31:47Z",
"timeoutUser": "",
"userID": "",
"usernameColor": "#ff000000"
}
]
}
@@ -0,0 +1,104 @@
{
"input": {
"broadcaster_user_id": "11148817",
"broadcaster_user_login": "pajlada",
"broadcaster_user_name": "pajlada",
"message": {
"fragments": [
{
"cheermote": null,
"emote": null,
"text": "penis ",
"type": "text"
},
{
"cheermote": null,
"emote": null,
"text": "ass penis",
"type": "text"
}
],
"text": "penis ass penis"
},
"message_id": "fc90dc3b-0634-41c3-8365-f40719f076ab",
"status": "denied",
"user_id": "159849156",
"user_login": "bajlada",
"user_name": "BajLada"
},
"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": [
"AutoMod:"
]
},
{
"color": "Text",
"flags": "Text",
"link": {
"type": "None",
"value": ""
},
"style": "ChatMedium",
"tooltip": "",
"trailingSpace": true,
"type": "TextElement",
"words": [
"Mods",
"have",
"denied",
"your",
"message."
]
}
],
"flags": "Timeout|PubSub|AutoMod|EventSub",
"id": "automod_fc90dc3b-0634-41c3-8365-f40719f076ab",
"localizedName": "",
"loginName": "automod",
"messageText": "AutoMod: Mods have denied your message.",
"searchText": "AutoMod: Mods have denied your message.",
"serverReceivedTime": "2024-05-14T12:31:47Z",
"timeoutUser": "",
"userID": "",
"usernameColor": "#ff000000"
}
]
}
+38
View File
@@ -120,6 +120,44 @@ const std::map<QString, std::string_view, QCompareCaseInsensitive>
"cost": 0
})",
},
{
"channel-chat-user-message-hold",
R"({
"id": "a3122e32-6498-4847-8675-109b9b94f29c",
"status": "enabled",
"type": "channel.chat.user_message_hold",
"version": "1",
"condition": {
"broadcaster_user_id": "11148817",
"moderator_user_id": "489584266"
},
"transport": {
"method":"websocket",
"session_id":"AgoQ59RRLw0mS6S000QtK8f54BIGY2VsbC1j"
},
"created_at": "2025-02-28T15:55:37.85489173Z",
"cost": 0
})",
},
{
"channel-chat-user-message-update",
R"({
"id": "a3122e32-6498-4847-8675-109b9b94f29c",
"status": "enabled",
"type": "channel.chat.user_message_update",
"version": "1",
"condition": {
"broadcaster_user_id": "11148817",
"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