feat: improve handling of shared chat messages (#5606)

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
iProdigy 2024-10-05 10:31:52 +00:00 committed by GitHub
parent 81d72db76b
commit 06d9a37709
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 199 additions and 33 deletions

View file

@ -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)

View file

@ -35,6 +35,7 @@ const QMap<QString, Type> 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()},

View file

@ -43,6 +43,7 @@ const QMap<QString, QString> 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"},

View file

@ -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<TwitchChannel *>(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<EmotePtr>{};
bool zeroWidth = false;
return this->twitchChannel;
}
std::tuple<std::optional<EmotePtr>, 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<EmotePtr> 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)
{

View file

@ -15,6 +15,7 @@
#include <ctime>
#include <memory>
#include <optional>
#include <tuple>
#include <unordered_map>
#include <utility>
@ -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<std::optional<EmotePtr>, MessageElementFlags, bool> parseEmote(
const EmoteName &name) const;
Outcome tryAppendEmote(const EmoteName &name);
void addWords(const QStringList &words,

View file

@ -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<MessageFlag>;

View file

@ -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);
}
}
}

View file

@ -456,6 +456,27 @@ std::vector<MessagePtr> 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<MessagePtr> 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<MessagePtr> 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;

View file

@ -1869,7 +1869,7 @@ std::optional<EmotePtr> TwitchChannel::ffzCustomVipBadge() const
return this->ffzCustomVipBadge_.get();
}
std::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string)
std::optional<CheerEmote> TwitchChannel::cheerEmote(const QString &string) const
{
auto sets = this->cheerEmoteSets_.access();
for (const auto &set : *sets)

View file

@ -197,7 +197,7 @@ public:
std::vector<FfzBadges::Badge> ffzChannelBadges(const QString &userID) const;
// Cheers
std::optional<CheerEmote> cheerEmote(const QString &string);
std::optional<CheerEmote> cheerEmote(const QString &string) const;
// Replies
/**

View file

@ -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;
}