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
11 changed files with 199 additions and 33 deletions
+1
View File
@@ -6,6 +6,7 @@
- Major: Release plugins alpha. (#5288) - Major: Release plugins alpha. (#5288)
- Major: Improve high-DPI support on Windows. (#4868, #5391) - 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: 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: Moved tab visibility control to a submenu, without any toggle actions. (#5530)
- Minor: Add option to customise Moderation buttons with images. (#5369) - 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) - Minor: Colored usernames now update on the fly when changing the "Color @usernames" setting. (#5300)
+3
View File
@@ -35,6 +35,7 @@ const QMap<QString, Type> MESSAGE_TYPING_CONTEXT{
{"flags.automod", Type::Bool}, {"flags.automod", Type::Bool},
{"flags.restricted", Type::Bool}, {"flags.restricted", Type::Bool},
{"flags.monitored", Type::Bool}, {"flags.monitored", Type::Bool},
{"flags.shared", Type::Bool},
{"message.content", Type::String}, {"message.content", Type::String},
{"message.length", Type::Int}, {"message.length", Type::Int},
{"reward.title", Type::String}, {"reward.title", Type::String},
@@ -78,6 +79,7 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
* flags.automod * flags.automod
* flags.restricted * flags.restricted
* flags.monitored * flags.monitored
* flags.shared
* *
* message.content * message.content
* message.length * message.length
@@ -141,6 +143,7 @@ ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
{"flags.automod", m->flags.has(MessageFlag::AutoMod)}, {"flags.automod", m->flags.has(MessageFlag::AutoMod)},
{"flags.restricted", m->flags.has(MessageFlag::RestrictedMessage)}, {"flags.restricted", m->flags.has(MessageFlag::RestrictedMessage)},
{"flags.monitored", m->flags.has(MessageFlag::MonitoredMessage)}, {"flags.monitored", m->flags.has(MessageFlag::MonitoredMessage)},
{"flags.shared", m->flags.has(MessageFlag::SharedMessage)},
{"message.content", m->messageText}, {"message.content", m->messageText},
{"message.length", m->messageText.length()}, {"message.length", m->messageText.length()},
@@ -43,6 +43,7 @@ const QMap<QString, QString> VALID_IDENTIFIERS_MAP{
{"flags.automod", "automod message?"}, {"flags.automod", "automod message?"},
{"flags.restricted", "restricted message?"}, {"flags.restricted", "restricted message?"},
{"flags.monitored", "monitored message?"}, {"flags.monitored", "monitored message?"},
{"flags.shared", "shared message?"},
{"message.content", "message text"}, {"message.content", "message text"},
{"message.length", "message length"}, {"message.length", "message length"},
{"reward.title", "point reward title"}, {"reward.title", "point reward title"},
+110 -31
View File
@@ -2677,6 +2677,26 @@ void MessageBuilder::parseRoomID()
{ {
this->twitchChannel->setRoomId(this->roomID_); 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(); return this->twitchChannel;
const auto *globalFfzEmotes = app->getFfzEmotes(); }
const auto *globalSeventvEmotes = app->getSeventvEmotes();
auto flags = MessageElementFlags();
auto emote = std::optional<EmotePtr>{};
bool zeroWidth = false;
std::tuple<std::optional<EmotePtr>, MessageElementFlags, bool>
MessageBuilder::parseEmote(const EmoteName &name) const
{
// Emote order: // Emote order:
// - FrankerFaceZ Channel // - FrankerFaceZ Channel
// - BetterTTV Channel // - BetterTTV Channel
@@ -2913,36 +2934,93 @@ Outcome MessageBuilder::tryAppendEmote(const EmoteName &name)
// - FrankerFaceZ Global // - FrankerFaceZ Global
// - BetterTTV Global // - BetterTTV Global
// - 7TV 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; return {
zeroWidth = emote.value()->zeroWidth; emote,
MessageElementFlag::BttvEmote,
zeroWidthEmotes.contains(name.string),
};
} }
else if ((emote = globalFfzEmotes->emote(name)))
emote = globalSeventvEmotes->globalEmote(name);
if (emote)
{ {
flags = MessageElementFlag::FfzEmote; return {
} emote,
else if ((emote = globalBttvEmotes->emote(name))) MessageElementFlag::SevenTVEmote,
{ emote.value()->zeroWidth,
flags = MessageElementFlag::BttvEmote; };
zeroWidth = zeroWidthEmotes.contains(name.string);
}
else if ((emote = globalSeventvEmotes->globalEmote(name)))
{
flags = MessageElementFlag::SevenTVEmote;
zeroWidth = emote.value()->zeroWidth;
} }
return {
{},
{},
false,
};
}
Outcome MessageBuilder::tryAppendEmote(const EmoteName &name)
{
const auto [emote, flags, zeroWidth] = this->parseEmote(name);
if (emote) if (emote)
{ {
if (zeroWidth && getSettings()->enableZeroWidthEmotes && if (zeroWidth && getSettings()->enableZeroWidthEmotes &&
@@ -3128,7 +3206,8 @@ Outcome MessageBuilder::tryParseCheermote(const QString &string)
return Failure; return Failure;
} }
auto cheerOpt = this->twitchChannel->cheerEmote(string); const auto *chan = this->getSourceChannel();
auto cheerOpt = chan->cheerEmote(string);
if (!cheerOpt) if (!cheerOpt)
{ {
+13
View File
@@ -15,6 +15,7 @@
#include <ctime> #include <ctime>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <tuple>
#include <unordered_map> #include <unordered_map>
#include <utility> #include <utility>
@@ -164,7 +165,10 @@ public:
QString userName; QString userName;
/// The Twitch Channel the message was received in
TwitchChannel *twitchChannel = nullptr; TwitchChannel *twitchChannel = nullptr;
/// The Twitch Channel the message was sent in, according to the Shared Chat feature
TwitchChannel *sourceChannel = nullptr;
Message *operator->(); Message *operator->();
Message &message(); Message &message();
@@ -278,6 +282,15 @@ protected:
void appendChannelName(); void appendChannelName();
void appendUsername(); 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); Outcome tryAppendEmote(const EmoteName &name);
void addWords(const QStringList &words, void addWords(const QStringList &words,
+2
View File
@@ -50,6 +50,8 @@ enum class MessageFlag : std::int64_t {
MonitoredMessage = (1LL << 35), MonitoredMessage = (1LL << 35),
/// The message is an ACTION message (/me) /// The message is an ACTION message (/me)
Action = (1LL << 36), 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>; using MessageFlags = FlagsEnum<MessageFlag>;
@@ -58,6 +58,10 @@ MessageFlagsPredicate::MessageFlagsPredicate(const QString &flags, bool negate)
{ {
this->flags_.set(MessageFlag::MonitoredMessage); 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(); auto parameters = message->parameters();
QString msgType = tags.value("msg-id").toString(); 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; QString content;
if (parameters.size() >= 2) if (parameters.size() >= 2)
{ {
@@ -483,6 +504,10 @@ std::vector<MessagePtr> parseUserNoticeMessage(Channel *channel,
MessageBuilder builder(channel, message, args, content, false); MessageBuilder builder(channel, message, args, content, false);
builder->flags.set(MessageFlag::Subscription); builder->flags.set(MessageFlag::Subscription);
builder->flags.unset(MessageFlag::Highlighted); builder->flags.unset(MessageFlag::Highlighted);
if (mirrored)
{
builder->flags.set(MessageFlag::SharedMessage);
}
builtMessages.emplace_back(builder.build()); builtMessages.emplace_back(builder.build());
} }
} }
@@ -546,6 +571,10 @@ std::vector<MessagePtr> parseUserNoticeMessage(Channel *channel,
calculateMessageTime(message).time()); calculateMessageTime(message).time());
b->flags.set(MessageFlag::Subscription); b->flags.set(MessageFlag::Subscription);
if (mirrored)
{
b->flags.set(MessageFlag::SharedMessage);
}
auto newMessage = b.release(); auto newMessage = b.release();
builtMessages.emplace_back(newMessage); builtMessages.emplace_back(newMessage);
} }
@@ -954,6 +983,27 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message,
auto target = parameters[0]; auto target = parameters[0];
QString msgType = tags.value("msg-id").toString(); 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; QString content;
if (parameters.size() >= 2) if (parameters.size() >= 2)
{ {
@@ -1039,6 +1089,10 @@ void IrcMessageHandler::handleUserNoticeMessage(Communi::IrcMessage *message,
calculateMessageTime(message).time()); calculateMessageTime(message).time());
b->flags.set(MessageFlag::Subscription); b->flags.set(MessageFlag::Subscription);
if (mirrored)
{
b->flags.set(MessageFlag::SharedMessage);
}
auto newMessage = b.release(); auto newMessage = b.release();
QString channelName; QString channelName;
+1 -1
View File
@@ -1869,7 +1869,7 @@ std::optional<EmotePtr> TwitchChannel::ffzCustomVipBadge() const
return this->ffzCustomVipBadge_.get(); 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(); auto sets = this->cheerEmoteSets_.access();
for (const auto &set : *sets) for (const auto &set : *sets)
+1 -1
View File
@@ -197,7 +197,7 @@ public:
std::vector<FfzBadges::Badge> ffzChannelBadges(const QString &userID) const; std::vector<FfzBadges::Badge> ffzChannelBadges(const QString &userID) const;
// Cheers // Cheers
std::optional<CheerEmote> cheerEmote(const QString &string); std::optional<CheerEmote> cheerEmote(const QString &string) const;
// Replies // Replies
/** /**
+9
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=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/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"(@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; return list;
} }
@@ -116,6 +118,12 @@ const QStringList &getSampleMiscMessages()
// mod announcement // 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)", 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) // Hype Chat (Paid option for keeping a message in chat longer)
// no level // 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)", 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=;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/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"(@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; return list;
} }