feat: improve handling of shared chat messages (#5606)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -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()},
|
||||
|
||||
@@ -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"},
|
||||
|
||||
+110
-31
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user