From 06d9a37709a90bef2f17cfa7aae54aeb0dc9e301 Mon Sep 17 00:00:00 2001 From: iProdigy <8106344+iProdigy@users.noreply.github.com> Date: Sat, 5 Oct 2024 10:31:52 +0000 Subject: [PATCH] feat: improve handling of shared chat messages (#5606) Co-authored-by: Rasmus Karlsson --- CHANGELOG.md | 1 + src/controllers/filters/lang/Filter.cpp | 3 + src/controllers/filters/lang/Tokenizer.cpp | 1 + src/messages/MessageBuilder.cpp | 141 ++++++++++++++---- src/messages/MessageBuilder.hpp | 13 ++ src/messages/MessageFlag.hpp | 2 + src/messages/search/MessageFlagsPredicate.cpp | 4 + src/providers/twitch/IrcMessageHandler.cpp | 54 +++++++ src/providers/twitch/TwitchChannel.cpp | 2 +- src/providers/twitch/TwitchChannel.hpp | 2 +- src/util/SampleData.cpp | 9 ++ 11 files changed, 199 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47cc16cf..d92b3795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Major: Release plugins alpha. (#5288) - Major: Improve high-DPI support on Windows. (#4868, #5391) - Minor: Removed the Ctrl+Shift+L hotkey for toggling the "live only" tab visibility state. (#5530) +- Minor: Add support for Shared Chat messages. Shared chat messages can be filtered with the `flags.shared` filter variable, or with search using `is:shared`. Some messages like subscriptions are filtered on purpose to avoid confusion for the broadcaster. If you have both channels participating in Shared Chat open, only one of the message triggering your highlight will trigger. (#5606) - Minor: Moved tab visibility control to a submenu, without any toggle actions. (#5530) - Minor: Add option to customise Moderation buttons with images. (#5369) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300) diff --git a/src/controllers/filters/lang/Filter.cpp b/src/controllers/filters/lang/Filter.cpp index 7b9ffe38..a2e79150 100644 --- a/src/controllers/filters/lang/Filter.cpp +++ b/src/controllers/filters/lang/Filter.cpp @@ -35,6 +35,7 @@ const QMap MESSAGE_TYPING_CONTEXT{ {"flags.automod", Type::Bool}, {"flags.restricted", Type::Bool}, {"flags.monitored", Type::Bool}, + {"flags.shared", Type::Bool}, {"message.content", Type::String}, {"message.length", Type::Int}, {"reward.title", Type::String}, @@ -78,6 +79,7 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) * flags.automod * flags.restricted * flags.monitored + * flags.shared * * message.content * message.length @@ -141,6 +143,7 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel) {"flags.automod", m->flags.has(MessageFlag::AutoMod)}, {"flags.restricted", m->flags.has(MessageFlag::RestrictedMessage)}, {"flags.monitored", m->flags.has(MessageFlag::MonitoredMessage)}, + {"flags.shared", m->flags.has(MessageFlag::SharedMessage)}, {"message.content", m->messageText}, {"message.length", m->messageText.length()}, diff --git a/src/controllers/filters/lang/Tokenizer.cpp b/src/controllers/filters/lang/Tokenizer.cpp index ce4f5c16..5e9d1137 100644 --- a/src/controllers/filters/lang/Tokenizer.cpp +++ b/src/controllers/filters/lang/Tokenizer.cpp @@ -43,6 +43,7 @@ const QMap VALID_IDENTIFIERS_MAP{ {"flags.automod", "automod message?"}, {"flags.restricted", "restricted message?"}, {"flags.monitored", "monitored message?"}, + {"flags.shared", "shared message?"}, {"message.content", "message text"}, {"message.length", "message length"}, {"reward.title", "point reward title"}, diff --git a/src/messages/MessageBuilder.cpp b/src/messages/MessageBuilder.cpp index d3750ea6..c391b3d3 100644 --- a/src/messages/MessageBuilder.cpp +++ b/src/messages/MessageBuilder.cpp @@ -2677,6 +2677,26 @@ void MessageBuilder::parseRoomID() { this->twitchChannel->setRoomId(this->roomID_); } + + if (auto it = this->tags.find("source-room-id"); it != this->tags.end()) + { + auto sourceRoom = it.value().toString(); + if (this->roomID_ != sourceRoom) + { + this->message().flags.set(MessageFlag::SharedMessage); + + auto sourceChan = + getApp()->getTwitch()->getChannelOrEmptyByID(sourceRoom); + if (sourceChan) + { + this->sourceChannel = + dynamic_cast(sourceChan.get()); + // avoid duplicate pings + this->message().flags.set( + MessageFlag::DoNotTriggerNotification); + } + } + } } } @@ -2894,18 +2914,19 @@ void MessageBuilder::appendUsername() } } -Outcome MessageBuilder::tryAppendEmote(const EmoteName &name) +const TwitchChannel *MessageBuilder::getSourceChannel() const { - auto *app = getApp(); + if (this->sourceChannel != nullptr) + { + return this->sourceChannel; + } - const auto *globalBttvEmotes = app->getBttvEmotes(); - const auto *globalFfzEmotes = app->getFfzEmotes(); - const auto *globalSeventvEmotes = app->getSeventvEmotes(); - - auto flags = MessageElementFlags(); - auto emote = std::optional{}; - bool zeroWidth = false; + return this->twitchChannel; +} +std::tuple, MessageElementFlags, bool> + MessageBuilder::parseEmote(const EmoteName &name) const +{ // Emote order: // - FrankerFaceZ Channel // - BetterTTV Channel @@ -2913,36 +2934,93 @@ Outcome MessageBuilder::tryAppendEmote(const EmoteName &name) // - FrankerFaceZ Global // - BetterTTV Global // - 7TV Global - if (this->twitchChannel && (emote = this->twitchChannel->ffzEmote(name))) + + const auto *globalFfzEmotes = getApp()->getFfzEmotes(); + const auto *globalBttvEmotes = getApp()->getBttvEmotes(); + const auto *globalSeventvEmotes = getApp()->getSeventvEmotes(); + + const auto *sourceChannel = this->getSourceChannel(); + + std::optional emote{}; + + if (sourceChannel != nullptr) { - flags = MessageElementFlag::FfzEmote; + // Check for channel emotes + + emote = sourceChannel->ffzEmote(name); + if (emote) + { + return { + emote, + MessageElementFlag::FfzEmote, + false, + }; + } + + emote = sourceChannel->bttvEmote(name); + if (emote) + { + return { + emote, + MessageElementFlag::BttvEmote, + false, + }; + } + + emote = sourceChannel->seventvEmote(name); + if (emote) + { + return { + emote, + MessageElementFlag::SevenTVEmote, + emote.value()->zeroWidth, + }; + } } - else if (this->twitchChannel && - (emote = this->twitchChannel->bttvEmote(name))) + + // Check for global emotes + + emote = globalFfzEmotes->emote(name); + if (emote) { - flags = MessageElementFlag::BttvEmote; + return { + emote, + MessageElementFlag::FfzEmote, + false, + }; } - else if (this->twitchChannel != nullptr && - (emote = this->twitchChannel->seventvEmote(name))) + + emote = globalBttvEmotes->emote(name); + if (emote) { - flags = MessageElementFlag::SevenTVEmote; - zeroWidth = emote.value()->zeroWidth; + return { + emote, + MessageElementFlag::BttvEmote, + zeroWidthEmotes.contains(name.string), + }; } - else if ((emote = globalFfzEmotes->emote(name))) + + emote = globalSeventvEmotes->globalEmote(name); + if (emote) { - flags = MessageElementFlag::FfzEmote; - } - else if ((emote = globalBttvEmotes->emote(name))) - { - flags = MessageElementFlag::BttvEmote; - zeroWidth = zeroWidthEmotes.contains(name.string); - } - else if ((emote = globalSeventvEmotes->globalEmote(name))) - { - flags = MessageElementFlag::SevenTVEmote; - zeroWidth = emote.value()->zeroWidth; + return { + emote, + MessageElementFlag::SevenTVEmote, + emote.value()->zeroWidth, + }; } + return { + {}, + {}, + false, + }; +} + +Outcome MessageBuilder::tryAppendEmote(const EmoteName &name) +{ + const auto [emote, flags, zeroWidth] = this->parseEmote(name); + if (emote) { if (zeroWidth && getSettings()->enableZeroWidthEmotes && @@ -3128,7 +3206,8 @@ Outcome MessageBuilder::tryParseCheermote(const QString &string) return Failure; } - auto cheerOpt = this->twitchChannel->cheerEmote(string); + const auto *chan = this->getSourceChannel(); + auto cheerOpt = chan->cheerEmote(string); if (!cheerOpt) { diff --git a/src/messages/MessageBuilder.hpp b/src/messages/MessageBuilder.hpp index 996595a8..651de404 100644 --- a/src/messages/MessageBuilder.hpp +++ b/src/messages/MessageBuilder.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -164,7 +165,10 @@ public: QString userName; + /// The Twitch Channel the message was received in TwitchChannel *twitchChannel = nullptr; + /// The Twitch Channel the message was sent in, according to the Shared Chat feature + TwitchChannel *sourceChannel = nullptr; Message *operator->(); Message &message(); @@ -278,6 +282,15 @@ protected: void appendChannelName(); void appendUsername(); + /// Return the Twitch Channel this message originated from + /// + /// Useful to handle messages from the "Shared Chat" feature + /// + /// Can return nullptr + const TwitchChannel *getSourceChannel() const; + + std::tuple, MessageElementFlags, bool> parseEmote( + const EmoteName &name) const; Outcome tryAppendEmote(const EmoteName &name); void addWords(const QStringList &words, diff --git a/src/messages/MessageFlag.hpp b/src/messages/MessageFlag.hpp index 7648dadc..60c213f0 100644 --- a/src/messages/MessageFlag.hpp +++ b/src/messages/MessageFlag.hpp @@ -50,6 +50,8 @@ enum class MessageFlag : std::int64_t { MonitoredMessage = (1LL << 35), /// The message is an ACTION message (/me) Action = (1LL << 36), + /// The message is sent in a different source channel as part of a Shared Chat session + SharedMessage = (1LL << 37), }; using MessageFlags = FlagsEnum; diff --git a/src/messages/search/MessageFlagsPredicate.cpp b/src/messages/search/MessageFlagsPredicate.cpp index d36fc72b..82a7748a 100644 --- a/src/messages/search/MessageFlagsPredicate.cpp +++ b/src/messages/search/MessageFlagsPredicate.cpp @@ -58,6 +58,10 @@ MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags, bool negate) { this->flags_.set(MessageFlag::MonitoredMessage); } + else if (flag == "shared") + { + this->flags_.set(MessageFlag::SharedMessage); + } } } diff --git a/src/providers/twitch/IrcMessageHandler.cpp b/src/providers/twitch/IrcMessageHandler.cpp index 279584c3..5dea75d2 100644 --- a/src/providers/twitch/IrcMessageHandler.cpp +++ b/src/providers/twitch/IrcMessageHandler.cpp @@ -456,6 +456,27 @@ std::vector parseUserNoticeMessage(Channel *channel, auto parameters = message->parameters(); QString msgType = tags.value("msg-id").toString(); + bool mirrored = msgType == "sharedchatnotice"; + if (mirrored) + { + msgType = tags.value("source-msg-id").toString(); + } + else + { + auto rIt = tags.find("room-id"); + auto sIt = tags.find("source-room-id"); + if (rIt != tags.end() && sIt != tags.end()) + { + mirrored = rIt.value().toString() != sIt.value().toString(); + } + } + + if (mirrored && msgType != "announcement") + { + // avoid confusing broadcasters with user payments to other channels + return {}; + } + QString content; if (parameters.size() >= 2) { @@ -483,6 +504,10 @@ std::vector parseUserNoticeMessage(Channel *channel, MessageBuilder builder(channel, message, args, content, false); builder->flags.set(MessageFlag::Subscription); builder->flags.unset(MessageFlag::Highlighted); + if (mirrored) + { + builder->flags.set(MessageFlag::SharedMessage); + } builtMessages.emplace_back(builder.build()); } } @@ -546,6 +571,10 @@ std::vector parseUserNoticeMessage(Channel *channel, calculateMessageTime(message).time()); b->flags.set(MessageFlag::Subscription); + if (mirrored) + { + b->flags.set(MessageFlag::SharedMessage); + } auto newMessage = b.release(); builtMessages.emplace_back(newMessage); } @@ -954,6 +983,27 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, auto target = parameters[0]; QString msgType = tags.value("msg-id").toString(); + bool mirrored = msgType == "sharedchatnotice"; + if (mirrored) + { + msgType = tags.value("source-msg-id").toString(); + } + else + { + auto rIt = tags.find("room-id"); + auto sIt = tags.find("source-room-id"); + if (rIt != tags.end() && sIt != tags.end()) + { + mirrored = rIt.value().toString() != sIt.value().toString(); + } + } + + if (mirrored && msgType != "announcement") + { + // avoid confusing broadcasters with user payments to other channels + return; + } + QString content; if (parameters.size() >= 2) { @@ -1039,6 +1089,10 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message, calculateMessageTime(message).time()); b->flags.set(MessageFlag::Subscription); + if (mirrored) + { + b->flags.set(MessageFlag::SharedMessage); + } auto newMessage = b.release(); QString channelName; diff --git a/src/providers/twitch/TwitchChannel.cpp b/src/providers/twitch/TwitchChannel.cpp index c54e3774..48bc1ee1 100644 --- a/src/providers/twitch/TwitchChannel.cpp +++ b/src/providers/twitch/TwitchChannel.cpp @@ -1869,7 +1869,7 @@ std::optional TwitchChannel::ffzCustomVipBadge() const return this->ffzCustomVipBadge_.get(); } -std::optional TwitchChannel::cheerEmote(const QString &string) +std::optional TwitchChannel::cheerEmote(const QString &string) const { auto sets = this->cheerEmoteSets_.access(); for (const auto &set : *sets) diff --git a/src/providers/twitch/TwitchChannel.hpp b/src/providers/twitch/TwitchChannel.hpp index 20f43079..fd2edaf7 100644 --- a/src/providers/twitch/TwitchChannel.hpp +++ b/src/providers/twitch/TwitchChannel.hpp @@ -197,7 +197,7 @@ public: std::vector ffzChannelBadges(const QString &userID) const; // Cheers - std::optional cheerEmote(const QString &string); + std::optional cheerEmote(const QString &string) const; // Replies /** diff --git a/src/util/SampleData.cpp b/src/util/SampleData.cpp index 7b12cd31..bc26fee0 100644 --- a/src/util/SampleData.cpp +++ b/src/util/SampleData.cpp @@ -57,6 +57,8 @@ const QStringList &getSampleCheerMessages() R"(@badge-info=;badges=bits/1;bits=1;color=#00FF7F;display-name=Baekjoon;emotes=;flags=;id=da47f91a-40d3-4209-ba1c-0219d8b8ecaf;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567440720363;turbo=0;user-id=73587716;user-type= :baekjoon!baekjoon@baekjoon.tmi.twitch.tv PRIVMSG #pajlada :Scoops1)", R"(@badge-info=;badges=bits/1;bits=10;color=#8A2BE2;display-name=EkimSky;emotes=;flags=;id=8adea5b4-7430-44ea-a666-5ebaceb69441;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567833047623;turbo=0;user-id=42132818;user-type= :ekimsky!ekimsky@ekimsky.tmi.twitch.tv PRIVMSG #pajlada :Hi Cheer10)", R"(@badge-info=;badges=bits-leader/2;bits=500;color=;display-name=godkiller76;emotes=;flags=;id=80e86bcc-d048-44f3-8073-9a1014568e0c;mod=0;room-id=111448817;subscriber=0;tmi-sent-ts=1567753685704;turbo=0;user-id=258838478;user-type= :godkiller76!godkiller76@godkiller76.tmi.twitch.tv PRIVMSG #pajlada :Party100 Party100 Party100 Party100 Party100)", + R"(@mod=0;flags=;badge-info=;source-badge-info=;color=#DAA520;user-id=612865661;subscriber=0;id=886028cc-9985-47b9-a273-8164c6d59a76;turbo=0;source-badges=staff/1,moderator/1,twitch-recap-2023/1;room-id=11148817;source-id=eefbae4a-d3a1-4307-8d15-fab0f03fd9b9;source-room-id=1025594235;emotes=;display-name=lahoooo;tmi-sent-ts=1727304317562;badges=staff/1,raging-wolf-helm/1;user-type=staff;bits=1 :lahoooo!lahoooo@lahoooo.tmi.twitch.tv PRIVMSG #pajlada Cheer1)", + R"(@id=7bf90f3f-75de-4e89-ab3d-2fdfefd6bfb1;source-id=590821fd-4a5c-4dd8-b27e-9cea4ffb8d87;source-badges=staff/1,moderator/1,bits-leader/3;user-id=612865661;badges=staff/1,raging-wolf-helm/1;emotes=;source-badge-info=;badge-info=;mod=0;source-room-id=1025594235;user-type=staff;color=#DAA520;tmi-sent-ts=1727375798676;display-name=lahoooo;bits=1;flags=;turbo=0;room-id=11148817;subscriber=0 :lahoooo!lahoooo@lahoooo.tmi.twitch.tv PRIVMSG #pajlada shared9Cheer1)", }; return list; } @@ -116,6 +118,12 @@ const QStringList &getSampleMiscMessages() // mod announcement R"(@badge-info=subscriber/47;badges=broadcaster/1,subscriber/3012,twitchconAmsterdam2020/1;color=#FF0000;display-name=Supinic;emotes=;flags=;id=8c26e1ab-b50c-4d9d-bc11-3fd57a941d90;login=supinic;mod=0;msg-id=announcement;msg-param-color=PRIMARY;room-id=31400525;subscriber=1;system-msg=;tmi-sent-ts=1648762219962;user-id=31400525;user-type= :tmi.twitch.tv USERNOTICE #supinic :mm test lol)", + // mod announcement from another channel + R"(@badge-info=;badges=staff/1,raging-wolf-helm/1;color=#DAA520;display-name=lahoooo;emotes=;flags=;id=01cd601f-bc3f-49d5-ab4b-136fa9d6ec22;login=lahoooo;mod=0;msg-id=sharedchatnotice;msg-param-color=PRIMARY;room-id=11148817;source-badge-info=;source-badges=staff/1,moderator/1,bits-leader/1;source-id=4083dadc-9f20-40f9-ba92-949ebf6bc294;source-msg-id=announcement;source-room-id=1025594235;subscriber=0;system-msg=;tmi-sent-ts=1726118378465;user-id=612865661;user-type=staff;vip=0 :tmi.twitch.tv USERNOTICE #pajlada :hi this is an announcement from 1)", + + // shared chat message + R"(@badge-info=;flags=;room-id=11148817;color=;client-nonce=0d1632f37b6baee51576859d5dbaf325;emotes=;subscriber=0;tmi-sent-ts=1727395701680;id=19ee1663-c14d-41cd-a4a2-30a4bb609c5a;turbo=1;badges=staff/1,turbo/1;source-badges=staff/1,moderator/1,bits-leader/1;source-badge-info=;display-name=creativewind;source-room-id=1025594235;source-id=b97eea45-f9dc-4f0c-8744-f8256c3ed950;user-type=staff;user-id=106940612;mod=0 :creativewind!creativewind@creativewind.tmi.twitch.tv PRIVMSG #pajlada :Guys can you please not share the chat. My mom bought me this new laptop and it gets really hot when the chat is being shared. Now my leg is starting to hurt because it is getting so hot. Please, if you don't want me to get burned, then dont share the chat.)", + // Hype Chat (Paid option for keeping a message in chat longer) // no level R"(@badge-info=subscriber/3;badges=subscriber/0,bits-charity/1;color=#0000FF;display-name=SnoopyTheBot;emotes=;first-msg=0;flags=;id=8779a9e5-cf1b-47b3-b9fe-67a5b1b605f6;mod=0;pinned-chat-paid-amount=500;pinned-chat-paid-canonical-amount=5;pinned-chat-paid-currency=USD;pinned-chat-paid-exponent=2;returning-chatter=0;room-id=36340781;subscriber=1;tmi-sent-ts=1664505974154;turbo=0;user-id=136881249;user-type= :snoopythebot!snoopythebot@snoopythebot.tmi.twitch.tv PRIVMSG #pajlada :-$5)", @@ -139,6 +147,7 @@ const QStringList &getSampleEmoteTestMessages() R"(@badge-info=;badges=moderator/1,partner/1;color=#5B99FF;display-name=StreamElements;emotes=86:30-39/822112:73-79;flags=22-27:S.5;id=03c3eec9-afd1-4858-a2e0-fccbf6ad8d1a;mod=1;room-id=11148817;subscriber=0;tmi-sent-ts=1588638345928;turbo=0;user-id=100135110;user-type=mod :streamelements!streamelements@streamelements.tmi.twitch.tv PRIVMSG #pajlada :╔ACTION A LOJA AINDA NÃO ESTÁ PRONTA BibleThump , AGUARDE... NOVIDADES EM BREVE FortOne╔)", R"(@badge-info=subscriber/20;badges=moderator/1,subscriber/12;color=#19E6E6;display-name=randers;emotes=25:39-43;flags=;id=3ea97f01-abb2-4acf-bdb8-f52e79cd0324;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1588837097115;turbo=0;user-id=40286300;user-type=mod :randers!randers@randers.tmi.twitch.tv PRIVMSG #pajlada :Då kan du begära skadestånd och förtal Kappa)", R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=41:6-13,15-22;flags=;id=a3196c7e-be4c-4b49-9c5a-8b8302b50c2a;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922213730;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm,Kreygasm (no space))", + R"(@client-nonce=9e118ff9b63fbbeea66520f37929685d;source-id=040d1673-826d-48d1-9728-badc945e4a5e;user-type=staff;user-id=612865661;turbo=0;id=3ece90ea-9aaa-4634-bf3a-0105185da505;tmi-sent-ts=1727304403389;color=#DAA520;source-room-id=1025594235;flags=;emotes=emotesv2_8811dd848a214cef8a77575476cc33f4:0-9;subscriber=0;mod=0;emote-only=1;badges=staff/1,raging-wolf-helm/1;room-id=11148817;display-name=lahoooo;badge-info=;source-badge-info=;source-badges=staff/1,moderator/1,bits-leader/3 :lahoooo!lahoooo@lahoooo.tmi.twitch.tv PRIVMSG #pajlada shared9Dog)", }; return list; }