Add option to check if chat was cleared / message was removed

This commit is contained in:
WarmUpTill 2025-06-03 20:20:01 +02:00 committed by WarmUpTill
parent 34151e4bc6
commit 0736d673e1
5 changed files with 238 additions and 74 deletions

View File

@ -730,6 +730,8 @@ AdvSceneSwitcher.condition.twitch.type.polling.channel.live="Stream is currently
AdvSceneSwitcher.condition.twitch.type.polling.channel.title="Current title matches"
AdvSceneSwitcher.condition.twitch.type.polling.channel.category="Current category is"
AdvSceneSwitcher.condition.twitch.type.chat.message="Chat message received"
AdvSceneSwitcher.condition.twitch.type.chat.messageRemoved="Single chat message removed"
AdvSceneSwitcher.condition.twitch.type.chat.cleared="All messages of user or channel cleared"
AdvSceneSwitcher.condition.twitch.type.chat.properties="Chat message properties:"
AdvSceneSwitcher.condition.twitch.type.chat.properties.select="Select chat message property:"
AdvSceneSwitcher.condition.twitch.type.chat.properties.firstMessage="Is first message"
@ -1920,6 +1922,24 @@ AdvSceneSwitcher.tempVar.twitch.is_turbo.chatReceive.description="'true' if the
AdvSceneSwitcher.tempVar.twitch.is_vip.chatReceive="Is chatter a VIP"
AdvSceneSwitcher.tempVar.twitch.is_vip.chatReceive.description="'true' if the chatter is a VIP in the channel, 'false' otherwise."
AdvSceneSwitcher.tempVar.twitch.login.chatRemove="User login"
AdvSceneSwitcher.tempVar.twitch.login.chatRemove.description="The name of the user who sent the message."
AdvSceneSwitcher.tempVar.twitch.message.chatRemove="Chat message"
AdvSceneSwitcher.tempVar.twitch.message_id.chatRemove.description="The chat message that was removed."
AdvSceneSwitcher.tempVar.twitch.message_id.chatRemove="Chat message ID"
AdvSceneSwitcher.tempVar.twitch.message_id.chatRemove.description="The ID of the message in UUID format."
AdvSceneSwitcher.tempVar.twitch.timestamp.chatRemove="Chat message timestamp"
AdvSceneSwitcher.tempVar.twitch.timestamp.chatRemove.description="The UNIX timestamp."
AdvSceneSwitcher.tempVar.twitch.login.chatClear="User login"
AdvSceneSwitcher.tempVar.twitch.login.chatClear.description="The login name of the user whose messages were removed from the chat room because they were banned or put in a timeout."
AdvSceneSwitcher.tempVar.twitch.user_id.chatClear="User ID"
AdvSceneSwitcher.tempVar.twitch.user_id.chatClear.description="Optional.\nThe User ID of the user that was banned or put in a timeout.\nEmpty in case the complete chat was cleared."
AdvSceneSwitcher.tempVar.twitch.ban_duration.chatClear="Ban duration"
AdvSceneSwitcher.tempVar.twitch.ban_duration.chatClear.description="Set to zero if the complete chat was cleared or a user was banned indefinitely.\nIs set to non-zero value, if a user was put in a timeout.\nWill contain the duration of the timeout in seconds."
AdvSceneSwitcher.tempVar.twitch.timestamp.chatClear="Timestamp"
AdvSceneSwitcher.tempVar.twitch.timestamp.chatClear.description="The UNIX timestamp."
AdvSceneSwitcher.tempVar.twitch.user_login.chatJoin="User login"
AdvSceneSwitcher.tempVar.twitch.user_login.chatJoin.description="The user login of the person who joined the chat room."

View File

@ -118,12 +118,17 @@ static void parseTags(const std::string &tags, IRCMessage &message)
message.properties.timestamp = std::stoull(tagValue);
} else if (tagName == "turbo") {
message.properties.isTurbo = tagValue == "1";
} else if (tagName == "user-id") {
} else if (tagName == "user-id" ||
tagName == "target-user-id" || tagName == "login") {
message.properties.userId = tagValue;
} else if (tagName == "user-type") {
message.properties.userType = tagValue;
} else if (tagName == "vip") {
message.properties.isVIP = tagValue == "1";
} else if (tagName == "ban-duration") {
message.properties.banDuration = std::stoull(tagValue);
} else if (tagName == "target-msg-id") {
message.properties.id = tagValue;
}
}
}
@ -172,34 +177,54 @@ static void parseCommand(const std::string &rawCommandComponent,
if (commandParts.size() < 3) {
return;
}
message.type = IRCMessage::Type::OTHER;
message.command.parameters = (commandParts[2] == "ACK");
} else if (message.command.command == "RECONNECT") {
blog(LOG_INFO,
"The Twitch IRC server is about to terminate the connection for maintenance.");
message.type = IRCMessage::Type::OTHER;
} else if (message.command.command == "421") {
// Unsupported command
message.type = IRCMessage::Type::OTHER;
return;
} else if (message.command.command == "PING" ||
message.command.command == "001" ||
message.command.command == "NOTICE" ||
message.command.command == "CLEARCHAT" ||
message.command.command == "HOSTTARGET" ||
message.command.command == "PRIVMSG") {
} else if (message.command.command == "PRIVMSG") {
if (commandParts.size() < 2) {
return;
}
message.type = IRCMessage::Type::MESSAGE_RECEIVED;
message.command.parameters = commandParts[1];
} else if (message.command.command == "CLEARCHAT") {
if (commandParts.size() < 1) {
return;
}
message.type = IRCMessage::Type::MESSAGE_CLEARED;
message.command.parameters = commandParts[1];
} else if (message.command.command == "CLEARMSG") {
if (commandParts.size() < 2) {
return;
}
message.type = IRCMessage::Type::MESSAGE_REMOVED;
message.command.parameters = commandParts[1];
} else if (message.command.command == "PING" ||
message.command.command == "001" ||
message.command.command == "NOTICE" ||
message.command.command == "HOSTTARGET") {
if (commandParts.size() < 2) {
return;
}
message.type = IRCMessage::Type::OTHER;
message.command.parameters = commandParts[1];
} else if (message.command.command == "JOIN") {
if (commandParts.size() < 2) {
return;
}
message.properties.joinedChannel = true;
message.type = IRCMessage::Type::USER_JOIN;
message.command.parameters = commandParts[1];
} else if (message.command.command == "PART") {
if (commandParts.size() < 2) {
return;
}
message.properties.leftChannel = true;
message.type = IRCMessage::Type::USER_LEAVE;
message.command.parameters = commandParts[1];
} else if (message.command.command == "GLOBALUSERSTATE" ||
message.command.command == "USERSTATE" ||
@ -213,7 +238,9 @@ static void parseCommand(const std::string &rawCommandComponent,
message.command.command == "375" ||
message.command.command == "376") {
// Do nothing for these cases for now
message.type = IRCMessage::Type::OTHER;
} else {
message.type = IRCMessage::Type::UNKNOWN;
vblog(LOG_INFO, "Unexpected IRC command: %s",
message.command.command.c_str());
}
@ -550,6 +577,18 @@ void TwitchChatConnection::HandleNewMessage(const IRCMessage &message)
message.message.c_str());
}
void TwitchChatConnection::HandleRemoveMessage(const IRCMessage &message)
{
_messageDispatcher.DispatchMessage(message);
vblog(LOG_INFO, "Chat message was removed");
}
void TwitchChatConnection::HandleClear(const IRCMessage &message)
{
_messageDispatcher.DispatchMessage(message);
vblog(LOG_INFO, "Chat was cleared");
}
void TwitchChatConnection::HandleWhisper(const IRCMessage &message)
{
_whisperDispatcher.DispatchMessage(message);
@ -610,6 +649,8 @@ void TwitchChatConnection::OnMessage(
static constexpr std::string_view reconnectCommand = "RECONNECT";
static constexpr std::string_view newMessageCommand = "PRIVMSG";
static constexpr std::string_view whisperCommand = "WHISPER";
static constexpr std::string_view clearCommand = "CLEARCHAT";
static constexpr std::string_view removeCommand = "CLEARMSG";
if (!message) {
return;
@ -636,6 +677,10 @@ void TwitchChatConnection::OnMessage(
HandlePart(message);
} else if (message.command.command == newMessageCommand) {
HandleNewMessage(message);
} else if (message.command.command == clearCommand) {
HandleClear(message);
} else if (message.command.command == removeCommand) {
HandleRemoveMessage(message);
} else if (message.command.command == whisperCommand) {
HandleWhisper(message);
} else if (message.command.command == noticeCommand) {

View File

@ -15,10 +15,23 @@ namespace advss {
using websocketpp::connection_hdl;
struct IRCMessage {
enum class Type {
MESSAGE_RECEIVED,
MESSAGE_REMOVED,
MESSAGE_CLEARED,
USER_LEAVE,
USER_JOIN,
OTHER,
UNKNOWN,
};
Type type = Type::UNKNOWN;
struct Badge {
std::string name;
bool enabled;
};
struct {
std::string badgeInfoString;
std::string badgesString;
@ -39,13 +52,12 @@ struct IRCMessage {
std::string rootParentId;
std::string rootParentUserLogin;
bool isSubscriber = false;
unsigned long long timestamp;
unsigned long long timestamp = 0;
unsigned long long banDuration = 0;
bool isTurbo = false;
std::string userId;
std::string userType;
bool isVIP = false;
bool joinedChannel = false;
bool leftChannel = false;
} properties;
struct {
@ -99,6 +111,8 @@ private:
void HandleJoin(const IRCMessage &);
void HandlePart(const IRCMessage &);
void HandleNewMessage(const IRCMessage &);
void HandleRemoveMessage(const IRCMessage &);
void HandleClear(const IRCMessage &);
void HandleWhisper(const IRCMessage &);
void HandleNotice(const IRCMessage &) const;
void HandleReconnect();

View File

@ -121,6 +121,10 @@ const static std::map<MacroConditionTwitch::Condition, std::string> conditionTyp
"AdvSceneSwitcher.condition.twitch.type.polling.channel.category"},
{MacroConditionTwitch::Condition::CHAT_MESSAGE_RECEIVED,
"AdvSceneSwitcher.condition.twitch.type.chat.message"},
{MacroConditionTwitch::Condition::CHAT_MESSAGE_REMOVED,
"AdvSceneSwitcher.condition.twitch.type.chat.messageRemoved"},
{MacroConditionTwitch::Condition::CHAT_CLEARED,
"AdvSceneSwitcher.condition.twitch.type.chat.cleared"},
{MacroConditionTwitch::Condition::CHAT_USER_JOINED,
"AdvSceneSwitcher.condition.twitch.type.chat.userJoined"},
{MacroConditionTwitch::Condition::CHAT_USER_LEFT,
@ -416,87 +420,128 @@ static bool stringMatches(const RegexConfig &regex, const std::string &string,
bool MacroConditionTwitch::CheckChatMessages(TwitchToken &token)
{
if (!_chatConnection) {
_chatConnection = TwitchChatConnection::GetChatConnection(
token, _channel);
if (!_chatConnection) {
return false;
}
_chatBuffer = _chatConnection->RegisterForMessages();
if (!ChatConnectionIsSetup(token)) {
return false;
}
while (!_chatBuffer->Empty()) {
auto message = _chatBuffer->ConsumeMessage();
if (!message) {
continue;
return HandleChatEvents([this](const IRCMessage &message) -> bool {
if (message.type != IRCMessage::Type::MESSAGE_RECEIVED) {
return false;
}
// Join and leave message don't have any message data
if (message->properties.leftChannel ||
message->properties.joinedChannel) {
continue;
if (!_chatMessagePattern.Matches(message)) {
return false;
}
if (!_chatMessagePattern.Matches(*message)) {
continue;
}
SetTempVarValue("id", message->properties.id);
SetTempVarValue("chat_message", message->message);
SetTempVarValue("user_id", message->properties.userId);
SetTempVarValue("user_login", message->source.nick);
SetTempVarValue("user_name", message->properties.displayName);
SetTempVarValue("user_type", message->properties.userType);
SetTempVarValue("id", message.properties.id);
SetTempVarValue("chat_message", message.message);
SetTempVarValue("user_id", message.properties.userId);
SetTempVarValue("user_login", message.source.nick);
SetTempVarValue("user_name", message.properties.displayName);
SetTempVarValue("user_type", message.properties.userType);
SetTempVarValue("reply_parent_id",
message->properties.replyParentId);
message.properties.replyParentId);
SetTempVarValue("reply_parent_message",
message->properties.replyParentBody);
message.properties.replyParentBody);
SetTempVarValue("reply_parent_user_id",
message->properties.replyParentUserId);
message.properties.replyParentUserId);
SetTempVarValue("reply_parent_user_login",
message->properties.replyParentUserLogin);
message.properties.replyParentUserLogin);
SetTempVarValue("reply_parent_user_name",
message->properties.replyParentDisplayName);
message.properties.replyParentDisplayName);
SetTempVarValue("root_parent_id",
message->properties.rootParentId);
message.properties.rootParentId);
SetTempVarValue("root_parent_user_login",
message->properties.rootParentUserLogin);
message.properties.rootParentUserLogin);
SetTempVarValue("badge_info",
message->properties.badgeInfoString);
SetTempVarValue("badges", message->properties.badgesString);
message.properties.badgeInfoString);
SetTempVarValue("badges", message.properties.badgesString);
SetTempVarValue("bits",
std::to_string(message->properties.bits));
SetTempVarValue("color", message->properties.color);
SetTempVarValue("emotes", message->properties.emotesString);
std::to_string(message.properties.bits));
SetTempVarValue("color", message.properties.color);
SetTempVarValue("emotes", message.properties.emotesString);
SetTempVarValue("timestamp",
std::to_string(message->properties.timestamp));
std::to_string(message.properties.timestamp));
SetTempVarValue("is_emotes_only",
message->properties.isUsingOnlyEmotes
? "true"
: "false");
message.properties.isUsingOnlyEmotes ? "true"
: "false");
SetTempVarValue("is_first_message",
message->properties.isFirstMessage ? "true"
: "false");
message.properties.isFirstMessage ? "true"
: "false");
SetTempVarValue("is_mod",
message->properties.isMod ? "true" : "false");
SetTempVarValue("is_subscriber",
message->properties.isSubscriber ? "true"
: "false");
message.properties.isMod ? "true" : "false");
SetTempVarValue("is_subscriber", message.properties.isSubscriber
? "true"
: "false");
SetTempVarValue("is_turbo",
message->properties.isTurbo ? "true" : "false");
message.properties.isTurbo ? "true" : "false");
SetTempVarValue("is_vip",
message->properties.isVIP ? "true" : "false");
if (_clearBufferOnMatch) {
_chatBuffer->Clear();
}
message.properties.isVIP ? "true" : "false");
return true;
}
return false;
});
}
bool MacroConditionTwitch::CheckChatUserJoinOrLeave(TwitchToken &token)
{
if (!ChatConnectionIsSetup(token)) {
return false;
}
return HandleChatEvents([this](const IRCMessage &message) -> bool {
if ((_condition == Condition::CHAT_USER_JOINED &&
message.type != IRCMessage::Type::USER_JOIN) ||
(_condition == Condition::CHAT_USER_LEFT &&
message.type != IRCMessage::Type::USER_LEAVE)) {
return false;
}
SetTempVarValue("user_login", message.source.nick);
return true;
});
}
bool MacroConditionTwitch::CheckChatClear(TwitchToken &token)
{
if (!ChatConnectionIsSetup(token)) {
return false;
}
return HandleChatEvents([this](const IRCMessage &message) -> bool {
if (message.type != IRCMessage::Type::MESSAGE_CLEARED) {
return false;
}
SetTempVarValue("ban_duration",
std::to_string(message.properties.banDuration));
SetTempVarValue("login", message.message);
SetTempVarValue("user_id", message.properties.userId);
SetTempVarValue("timestamp",
std::to_string(message.properties.timestamp));
return true;
});
}
bool MacroConditionTwitch::CheckChatMessageRemove(TwitchToken &token)
{
if (!ChatConnectionIsSetup(token)) {
return false;
}
return HandleChatEvents([this](const IRCMessage &message) -> bool {
if (message.type != IRCMessage::Type::MESSAGE_REMOVED) {
return false;
}
SetTempVarValue("message", message.message);
SetTempVarValue("message_id", message.properties.id);
SetTempVarValue("login", message.properties.userId);
SetTempVarValue("timestamp",
std::to_string(message.properties.timestamp));
return true;
});
}
bool MacroConditionTwitch::ChatConnectionIsSetup(TwitchToken &token)
{
if (!_chatConnection) {
_chatConnection = TwitchChatConnection::GetChatConnection(
@ -507,22 +552,22 @@ bool MacroConditionTwitch::CheckChatUserJoinOrLeave(TwitchToken &token)
_chatBuffer = _chatConnection->RegisterForMessages();
return false;
}
return true;
}
bool MacroConditionTwitch::HandleChatEvents(
const std::function<bool(const IRCMessage &)> &matchCb)
{
while (!_chatBuffer->Empty()) {
auto message = _chatBuffer->ConsumeMessage();
if (!message) {
continue;
}
if ((_condition == Condition::CHAT_USER_JOINED &&
!message->properties.joinedChannel) ||
(_condition == Condition::CHAT_USER_LEFT &&
!message->properties.leftChannel)) {
if (!matchCb(*message)) {
continue;
}
SetTempVarValue("user_login", message->source.nick);
if (_clearBufferOnMatch) {
_chatBuffer->Clear();
}
@ -715,6 +760,20 @@ bool MacroConditionTwitch::CheckCondition()
}
return CheckChatMessages(*token);
}
case Condition::CHAT_CLEARED: {
auto token = _token.lock();
if (!token) {
return false;
}
return CheckChatClear(*token);
}
case Condition::CHAT_MESSAGE_REMOVED: {
auto token = _token.lock();
if (!token) {
return false;
}
return CheckChatMessageRemove(*token);
}
case Condition::CHAT_USER_JOINED:
case Condition::CHAT_USER_LEFT: {
auto token = _token.lock();
@ -871,6 +930,10 @@ bool MacroConditionTwitch::ConditionIsSupportedByToken()
{Condition::CATEGORY_POLLING, {}},
{Condition::CHAT_MESSAGE_RECEIVED,
{{"chat:read"}, {"chat:edit"}}},
{Condition::CHAT_MESSAGE_REMOVED,
{{"chat:read"}, {"chat:edit"}}},
{Condition::CHAT_CLEARED,
{{"chat:read"}, {"chat:edit"}}},
{Condition::CHAT_USER_JOINED,
{{"chat:read"}, {"chat:edit"}}},
{Condition::CHAT_USER_LEFT,
@ -1069,6 +1132,8 @@ void MacroConditionTwitch::SetupTempVars()
};
if (_condition != Condition::CHAT_MESSAGE_RECEIVED &&
_condition != Condition::CHAT_MESSAGE_REMOVED &&
_condition != Condition::CHAT_CLEARED &&
_condition != Condition::CHAT_USER_JOINED &&
_condition != Condition::CHAT_USER_LEFT &&
_condition != Condition::RAID_INBOUND_EVENT &&
@ -1423,6 +1488,18 @@ void MacroConditionTwitch::SetupTempVars()
setupTempVarHelper("is_turbo", ".chatReceive");
setupTempVarHelper("is_vip", ".chatReceive");
break;
case Condition::CHAT_MESSAGE_REMOVED:
setupTempVarHelper("message", ".chatRemove");
setupTempVarHelper("message_id", ".chatRemove");
setupTempVarHelper("login", ".chatRemove");
setupTempVarHelper("timestamp", ".chatRemove");
break;
case Condition::CHAT_CLEARED:
setupTempVarHelper("ban_duration", ".chatClear");
setupTempVarHelper("login", ".chatClear");
setupTempVarHelper("user_id", ".chatClear");
setupTempVarHelper("timestamp", ".chatClear");
break;
case Condition::CHAT_USER_JOINED:
setupTempVarHelper("user_login", ".chatJoin");
break;

View File

@ -74,6 +74,8 @@ public:
// Chat
CHAT_MESSAGE_RECEIVED = 500000,
CHAT_MESSAGE_REMOVED = 500030,
CHAT_CLEARED = 500060,
CHAT_USER_JOINED = 500100,
CHAT_USER_LEFT = 500200,
@ -113,10 +115,16 @@ private:
bool CheckChannelLiveEvents();
bool CheckChannelRewardChangeEvents();
bool CheckChannelRewardRedemptionEvents();
bool CheckChatMessages(TwitchToken &token);
bool HandleMatchingSubscriptionEvents(
const std::function<void(const Event &)> &matchCb);
bool CheckChatMessages(TwitchToken &token);
bool CheckChatUserJoinOrLeave(TwitchToken &token);
bool CheckChatClear(TwitchToken &token);
bool CheckChatMessageRemove(TwitchToken &token);
bool ChatConnectionIsSetup(TwitchToken &token);
bool HandleChatEvents(
const std::function<bool(const IRCMessage &)> &matchCb);
void RegisterEventSubscription();
void ResetSubscription();