diff --git a/data/locale/en-US.ini b/data/locale/en-US.ini index ccddb8fc..35927948 100644 --- a/data/locale/en-US.ini +++ b/data/locale/en-US.ini @@ -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." diff --git a/plugins/twitch/chat-connection.cpp b/plugins/twitch/chat-connection.cpp index 4c5f487d..6c1c4e6f 100644 --- a/plugins/twitch/chat-connection.cpp +++ b/plugins/twitch/chat-connection.cpp @@ -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) { diff --git a/plugins/twitch/chat-connection.hpp b/plugins/twitch/chat-connection.hpp index 03548bd7..3dc96338 100644 --- a/plugins/twitch/chat-connection.hpp +++ b/plugins/twitch/chat-connection.hpp @@ -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(); diff --git a/plugins/twitch/macro-condition-twitch.cpp b/plugins/twitch/macro-condition-twitch.cpp index 9af53ad4..5827afa4 100644 --- a/plugins/twitch/macro-condition-twitch.cpp +++ b/plugins/twitch/macro-condition-twitch.cpp @@ -121,6 +121,10 @@ const static std::map 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 ®ex, 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 &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; diff --git a/plugins/twitch/macro-condition-twitch.hpp b/plugins/twitch/macro-condition-twitch.hpp index bd13c037..85f6abc9 100644 --- a/plugins/twitch/macro-condition-twitch.hpp +++ b/plugins/twitch/macro-condition-twitch.hpp @@ -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 &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 &matchCb); void RegisterEventSubscription(); void ResetSubscription();