diff --git a/CHANGELOG.md b/CHANGELOG.md index 47cc16cf1..d92b37954 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 7b9ffe38f..a2e79150b 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 ce4f5c16d..5e9d11374 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 d3750ea63..c391b3d32 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 996595a8e..651de4045 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 7648dadc7..60c213f03 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 d36fc72bb..82a7748ad 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 279584c35..5dea75d21 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 c54e37741..48bc1ee19 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 20f43079e..fd2edaf79 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 7b12cd31c..bc26fee0b 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; }