Skip to content

Commit

Permalink
Update emote parsing (#1714)
Browse files Browse the repository at this point in the history
Fixes #1707
  • Loading branch information
pajlada committed Sep 26, 2020
1 parent d9e6488 commit 20e4d6b
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 81 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- Bugfix: /usercard command will now respect the "Automatically close user popup" setting (#1918)
- Bugfix: Handle symlinks properly when saving commands & settings (#1856, #1908)
- Bugfix: Starting Chatterino in a minimized state after an update will no longer cause a crash
- Bugfix: Modify the emote parsing to handle some edge-cases with dots and stuff (#1704, #1714)

## 2.2.0

Expand Down
183 changes: 112 additions & 71 deletions src/providers/twitch/TwitchMessageBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ MessagePtr TwitchMessageBuilder::build()
}

// twitch emotes
std::vector<std::tuple<int, EmotePtr, EmoteName>> twitchEmotes;
std::vector<TwitchEmoteOccurence> twitchEmotes;

iterator = this->tags.find("emotes");
if (iterator != this->tags.end())
Expand All @@ -307,12 +307,11 @@ MessagePtr TwitchMessageBuilder::build()

std::sort(twitchEmotes.begin(), twitchEmotes.end(),
[](const auto &a, const auto &b) {
return std::get<0>(a) < std::get<0>(b);
return a.start < b.start; //
});
twitchEmotes.erase(std::unique(twitchEmotes.begin(), twitchEmotes.end(),
[](const auto &first, const auto &second) {
return std::get<0>(first) ==
std::get<0>(second);
return first.start == second.start;
}),
twitchEmotes.end());

Expand All @@ -339,47 +338,90 @@ MessagePtr TwitchMessageBuilder::build()
return this->release();
}

bool doesWordContainATwitchEmote(
int cursor, const QString &word,
const std::vector<TwitchEmoteOccurence> &twitchEmotes,
std::vector<TwitchEmoteOccurence>::const_iterator &currentTwitchEmoteIt)
{
if (currentTwitchEmoteIt == twitchEmotes.end())
{
// No emote to add!
return false;
}

const auto &currentTwitchEmote = *currentTwitchEmoteIt;

auto wordEnd = cursor + word.length();

// Check if this emote fits within the word boundaries
if (currentTwitchEmote.start < cursor || currentTwitchEmote.end > wordEnd)
{
// this emote does not fit xd
return false;
}

return true;
}

void TwitchMessageBuilder::addWords(
const QStringList &words,
const std::vector<std::tuple<int, EmotePtr, EmoteName>> &twitchEmotes)
const std::vector<TwitchEmoteOccurence> &twitchEmotes)
{
auto i = int();
auto currentTwitchEmote = twitchEmotes.begin();
// cursor currently indicates what character index we're currently operating in the full list of words
int cursor = 0;
auto currentTwitchEmoteIt = twitchEmotes.begin();

for (auto word : words)
{
// check if it's a twitch emote twitch emote
while (currentTwitchEmote != twitchEmotes.end() &&
std::get<0>(*currentTwitchEmote) < i)
{
++currentTwitchEmote;
}
if (currentTwitchEmote != twitchEmotes.end() &&
std::get<0>(*currentTwitchEmote) == i)
while (doesWordContainATwitchEmote(cursor, word, twitchEmotes,
currentTwitchEmoteIt))
{
auto emoteImage = std::get<1>(*currentTwitchEmote);
if (emoteImage == nullptr)
auto wordEnd = cursor + word.length();
const auto &currentTwitchEmote = *currentTwitchEmoteIt;

if (currentTwitchEmote.start == cursor)
{
qDebug() << "emoteImage nullptr"
<< std::get<2>(*currentTwitchEmote).string;
}
this->emplace<EmoteElement>(emoteImage,
MessageElementFlag::TwitchEmote);
// This emote exists right at the start of the word!
this->emplace<EmoteElement>(currentTwitchEmote.ptr,
MessageElementFlag::TwitchEmote);
auto len = currentTwitchEmote.name.string.length();
cursor += len;
word = word.mid(len);

i += word.length() + 1;
++currentTwitchEmoteIt;

int len = std::get<2>(*currentTwitchEmote).string.length();
currentTwitchEmote++;
if (word.isEmpty())
{
// space
cursor += 1;
break;
}
else
{
this->message().elements.back()->setTrailingSpace(false);
}

if (len < word.length())
{
word = word.mid(len);
this->message().elements.back()->setTrailingSpace(false);
continue;
}
else

// Emote is not at the start

// 1. Add text before the emote
QString preText = word.left(currentTwitchEmote.start - cursor);
for (auto &variant : getApp()->emotes->emojis.parse(preText))
{
continue;
boost::apply_visitor(
[&](auto &&arg) { this->addTextOrEmoji(arg); }, variant);
}

cursor += preText.size();

word = word.mid(preText.size());
}

if (word.isEmpty())
{
continue;
}

// split words
Expand All @@ -389,7 +431,7 @@ void TwitchMessageBuilder::addWords(
variant);
}

i += word.size() + 1;
cursor += word.size() + 1;
}
}

Expand Down Expand Up @@ -660,36 +702,32 @@ void TwitchMessageBuilder::appendUsername()
}

void TwitchMessageBuilder::runIgnoreReplaces(
std::vector<std::tuple<int, EmotePtr, EmoteName>> &twitchEmotes)
std::vector<TwitchEmoteOccurence> &twitchEmotes)
{
auto phrases = getCSettings().ignoredMessages.readOnly();
auto removeEmotesInRange =
[](int pos, int len,
std::vector<std::tuple<int, EmotePtr, EmoteName>>
&twitchEmotes) mutable {
auto it =
std::partition(twitchEmotes.begin(), twitchEmotes.end(),
[pos, len](const auto &item) {
return !((std::get<0>(item) >= pos) &&
std::get<0>(item) < (pos + len));
});
for (auto copy = it; copy != twitchEmotes.end(); ++copy)
auto removeEmotesInRange = [](int pos, int len,
auto &twitchEmotes) mutable {
auto it = std::partition(
twitchEmotes.begin(), twitchEmotes.end(),
[pos, len](const auto &item) {
return !((item.start >= pos) && item.start < (pos + len));
});
for (auto copy = it; copy != twitchEmotes.end(); ++copy)
{
if ((*copy).ptr == nullptr)
{
if (std::get<1>(*copy) == nullptr)
{
qDebug() << "remem nullptr" << std::get<2>(*copy).string;
}
qDebug() << "remem nullptr" << (*copy).name.string;
}
std::vector<std::tuple<int, EmotePtr, EmoteName>> v(
it, twitchEmotes.end());
twitchEmotes.erase(it, twitchEmotes.end());
return v;
};
}
std::vector<TwitchEmoteOccurence> v(it, twitchEmotes.end());
twitchEmotes.erase(it, twitchEmotes.end());
return v;
};

auto shiftIndicesAfter = [&twitchEmotes](int pos, int by) mutable {
for (auto &item : twitchEmotes)
{
auto &index = std::get<0>(item);
auto &index = item.start;
if (index >= pos)
{
index += by;
Expand Down Expand Up @@ -717,8 +755,12 @@ void TwitchMessageBuilder::runIgnoreReplaces(
{
qDebug() << "emote null" << emote.first.string;
}
twitchEmotes.push_back(std::tuple<int, EmotePtr, EmoteName>{
startIndex + pos, emote.second, emote.first});
twitchEmotes.push_back(TwitchEmoteOccurence{
startIndex + pos,
startIndex + pos + emote.first.string.length(),
emote.second,
emote.first,
});
}
}
pos += word.length() + 1;
Expand Down Expand Up @@ -776,21 +818,21 @@ void TwitchMessageBuilder::runIgnoreReplaces(

for (auto &tup : vret)
{
if (std::get<1>(tup) == nullptr)
if (tup.ptr == nullptr)
{
qDebug() << "v nullptr" << std::get<2>(tup).string;
qDebug() << "v nullptr" << tup.name.string;
continue;
}
QRegularExpression emoteregex(
"\\b" + std::get<2>(tup).string + "\\b",
"\\b" + tup.name.string + "\\b",
QRegularExpression::UseUnicodePropertiesOption);
auto _match = emoteregex.match(midExtendedRef);
if (_match.hasMatch())
{
int last = _match.lastCapturedIndex();
for (int i = 0; i <= last; ++i)
{
std::get<0>(tup) = from + _match.capturedStart();
tup.start = from + _match.capturedStart();
twitchEmotes.push_back(std::move(tup));
}
}
Expand Down Expand Up @@ -845,21 +887,21 @@ void TwitchMessageBuilder::runIgnoreReplaces(

for (auto &tup : vret)
{
if (std::get<1>(tup) == nullptr)
if (tup.ptr == nullptr)
{
qDebug() << "v nullptr" << std::get<2>(tup).string;
qDebug() << "v nullptr" << tup.name.string;
continue;
}
QRegularExpression emoteregex(
"\\b" + std::get<2>(tup).string + "\\b",
"\\b" + tup.name.string + "\\b",
QRegularExpression::UseUnicodePropertiesOption);
auto match = emoteregex.match(midExtendedRef);
if (match.hasMatch())
{
int last = match.lastCapturedIndex();
for (int i = 0; i <= last; ++i)
{
std::get<0>(tup) = from + match.capturedStart();
tup.start = from + match.capturedStart();
twitchEmotes.push_back(std::move(tup));
}
}
Expand All @@ -874,8 +916,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
}

void TwitchMessageBuilder::appendTwitchEmote(
const QString &emote,
std::vector<std::tuple<int, EmotePtr, EmoteName>> &vec,
const QString &emote, std::vector<TwitchEmoteOccurence> &vec,
std::vector<int> &correctPositions)
{
auto app = getApp();
Expand Down Expand Up @@ -914,13 +955,13 @@ void TwitchMessageBuilder::appendTwitchEmote(

auto name =
EmoteName{this->originalMessage_.mid(start, end - start + 1)};
auto tup = std::tuple<int, EmotePtr, EmoteName>{
start, app->emotes->twitch.getOrCreateEmote(id, name), name};
if (std::get<1>(tup) == nullptr)
TwitchEmoteOccurence emoteOccurence{
start, end, app->emotes->twitch.getOrCreateEmote(id, name), name};
if (emoteOccurence.ptr == nullptr)
{
qDebug() << "nullptr" << std::get<2>(tup).string;
qDebug() << "nullptr" << emoteOccurence.name.string;
}
vec.push_back(std::move(tup));
vec.push_back(std::move(emoteOccurence));
}
}

Expand Down
23 changes: 14 additions & 9 deletions src/providers/twitch/TwitchMessageBuilder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ using EmotePtr = std::shared_ptr<const Emote>;
class Channel;
class TwitchChannel;

struct TwitchEmoteOccurence {
int start;
int end;
EmotePtr ptr;
EmoteName name;
};

class TwitchMessageBuilder : public SharedMessageBuilder
{
public:
Expand Down Expand Up @@ -52,19 +59,17 @@ class TwitchMessageBuilder : public SharedMessageBuilder
void parseMessageID();
void parseRoomID();
void appendUsername();
void runIgnoreReplaces(
std::vector<std::tuple<int, EmotePtr, EmoteName>> &twitchEmotes);

void runIgnoreReplaces(std::vector<TwitchEmoteOccurence> &twitchEmotes);

boost::optional<EmotePtr> getTwitchBadge(const Badge &badge);
void appendTwitchEmote(
const QString &emote,
std::vector<std::tuple<int, EmotePtr, EmoteName>> &vec,
std::vector<int> &correctPositions);
void appendTwitchEmote(const QString &emote,
std::vector<TwitchEmoteOccurence> &vec,
std::vector<int> &correctPositions);
Outcome tryAppendEmote(const EmoteName &name);

void addWords(
const QStringList &words,
const std::vector<std::tuple<int, EmotePtr, EmoteName>> &twitchEmotes);
void addWords(const QStringList &words,
const std::vector<TwitchEmoteOccurence> &twitchEmotes);
void addTextOrEmoji(EmotePtr emote) override;
void addTextOrEmoji(const QString &value) override;

Expand Down
18 changes: 17 additions & 1 deletion src/widgets/Window.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ void Window::addCustomTitlebarButtons()
void Window::addDebugStuff()
{
#ifdef C_DEBUG
std::vector<QString> cheerMessages, subMessages, miscMessages, linkMessages;
std::vector<QString> cheerMessages, subMessages, miscMessages, linkMessages,
emoteTestMessages;

cheerMessages = getSampleCheerMessage();
auto validLinks = getValidLinks();
Expand Down Expand Up @@ -222,6 +223,14 @@ void Window::addDebugStuff()
const char *channelRewardMessage2 = "{ \"type\": \"MESSAGE\", \"data\": { \"topic\": \"community-points-channel-v1.11148817\", \"message\": { \"type\": \"reward-redeemed\", \"data\": { \"timestamp\": \"2020-07-13T20:19:31.430785354Z\", \"redemption\": { \"id\": \"b9628798-1b4e-4122-b2a6-031658df6755\", \"user\": { \"id\": \"91800084\", \"login\": \"cranken1337\", \"display_name\": \"cranken1337\" }, \"channel_id\": \"11148817\", \"redeemed_at\": \"2020-07-13T20:19:31.345237005Z\", \"reward\": { \"id\": \"313969fe-cc9f-4a0a-83c6-172acbd96957\", \"channel_id\": \"11148817\", \"title\": \"annoying reward pogchamp\", \"prompt\": \"\", \"cost\": 3000, \"is_user_input_required\": false, \"is_sub_only\": false, \"image\": null, \"default_image\": { \"url_1x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-1.png\", \"url_2x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-2.png\", \"url_4x\": \"https://static-cdn.jtvnw.net/custom-reward-images/default-4.png\" }, \"background_color\": \"#52ACEC\", \"is_enabled\": true, \"is_paused\": false, \"is_in_stock\": true, \"max_per_stream\": { \"is_enabled\": false, \"max_per_stream\": 0 }, \"should_redemptions_skip_request_queue\": false, \"template_id\": null, \"updated_for_indicator_at\": \"2020-01-20T04:33:33.624956679Z\" }, \"status\": \"UNFULFILLED\", \"cursor\": \"Yjk2Mjg3OTgtMWI0ZS00MTIyLWIyYTYtMDMxNjU4ZGY2NzU1X18yMDIwLTA3LTEzVDIwOjE5OjMxLjM0NTIzNzAwNVo=\" } } } } }";
const char *channelRewardIRCMessage(R"(@badge-info=subscriber/43;badges=subscriber/42;color=#1E90FF;custom-reward-id=313969fe-cc9f-4a0a-83c6-172acbd96957;display-name=Cranken1337;emotes=;flags=;id=3cee3f27-a1d0-44d1-a606-722cebdad08b;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1594756484132;turbo=0;user-id=91800084;user-type= :cranken1337!cranken1337@cranken1337.tmi.twitch.tv PRIVMSG #pajlada :wow, amazing reward)");

emoteTestMessages.emplace_back(R"(@badge-info=subscriber/3;badges=subscriber/3;color=#0000FF;display-name=Linkoping;emotes=25:40-44;flags=17-26:S.6;id=744f9c58-b180-4f46-bd9e-b515b5ef75c1;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1566335866017;turbo=0;user-id=91673457;user-type= :linkoping!linkoping@linkoping.tmi.twitch.tv PRIVMSG #pajlada :Då kan du begära skadestånd och förtal Kappa)");
emoteTestMessages.emplace_back(R"(@badge-info=subscriber/1;badges=subscriber/0;color=;display-name=jhoelsc;emotes=301683486:46-58,60-72,74-86/301683544:88-100;flags=0-4:S.6;id=1f1afcdd-d94c-4699-b35f-d214deb1e11a;mod=0;room-id=11148817;subscriber=1;tmi-sent-ts=1588640587462;turbo=0;user-id=505763008;user-type= :jhoelsc!jhoelsc@jhoelsc.tmi.twitch.tv PRIVMSG #pajlada :pensé que no habría directo que bueno que si staryuukiLove staryuukiLove staryuukiLove staryuukiBits)");
emoteTestMessages.emplace_back(R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=41:6-13,16-23;flags=;id=97c28382-e8d2-45a0-bb5d-2305fc4ef139;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922036771;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm, Kreygasm)");
emoteTestMessages.emplace_back(R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=25:24-28/41:6-13,15-22;flags=;id=5a36536b-a952-43f7-9c41-88c829371b7a;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922039721;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kreygasm,Kreygasm Kappa (no space then space))");
emoteTestMessages.emplace_back(R"(@badge-info=subscriber/34;badges=moderator/1,subscriber/24;color=#FF0000;display-name=테스트계정420;emotes=25:6-10/1902:12-16/88:18-25;flags=;id=aed9e67e-f8cd-493e-aa6b-da054edd7292;mod=1;room-id=11148817;subscriber=1;tmi-sent-ts=1590922042881;turbo=0;user-id=117166826;user-type=mod :testaccount_420!testaccount_420@testaccount_420.tmi.twitch.tv PRIVMSG #pajlada :-tags Kappa.Keepo.PogChamp.extratext (3 emotes with extra text))");
emoteTestMessages.emplace_back(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╔)");
emoteTestMessages.emplace_back(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)");
emoteTestMessages.emplace_back(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))");
// clang-format on

createWindowShortcut(this, "F6", [=] {
Expand Down Expand Up @@ -268,6 +277,13 @@ void Window::addDebugStuff()
}
});

createWindowShortcut(this, "F11", [=] {
const auto &messages = emoteTestMessages;
static int index = 0;
const auto &msg = messages[index++ % messages.size()];
getApp()->twitch.server->addFakeMessage(msg);
});

#endif
} // namespace chatterino

Expand Down

0 comments on commit 20e4d6b

Please sign in to comment.