Skip to content

Commit

Permalink
fix: support captures in ignores (Chatterino#5126)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerixyz authored and devJimmyboy committed Feb 11, 2024
1 parent ee4e35d commit b95a6cf
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 22 deletions.
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@
- Bugfix: Fixed thread popup window missing messages for nested threads. (#4923)
- Bugfix: Fixed an occasional crash for channel point redemptions with text input. (#4949)
- Bugfix: Fixed triple click on message also selecting moderation buttons. (#4961)
- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965)
- Bugfix: Fixed a freeze from a bad regex in _Ignores_. (#4965, #5126)
- Bugfix: Fixed badge highlight changes not immediately being reflected. (#5110)
- Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965)
- Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965)
- Bugfix: Fixed some emotes not appearing when using _Ignores_. (#4965, #5126)
- Bugfix: Fixed lookahead/-behind not working in _Ignores_. (#4965, #5126)
- Bugfix: Fixed Image Uploader accidentally deleting images with some hosts when link resolver was enabled. (#4971)
- Bugfix: Fixed rare crash with Image Uploader when closing a split right after starting an upload. (#4971)
- Bugfix: Fixed an issue on macOS where the image uploader would keep prompting the user even after they clicked "Yes, don't ask again". (#5011)
Expand Down
164 changes: 147 additions & 17 deletions src/providers/twitch/TwitchMessageBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,128 @@ namespace {
builder->message().badgeInfos = badgeInfos;
}

/**
* Computes (only) the replacement of @a match in @a source.
* The parts before and after the match in @a source are ignored.
*
* Occurrences of \b{\\1}, \b{\\2}, ..., in @a replacement are replaced
* with the string captured by the corresponding capturing group.
* This function should only be used if the regex contains capturing groups.
*
* Since Qt doesn't provide a way of replacing a single match with some replacement
* while supporting both capturing groups and lookahead/-behind in the regex,
* this is included here. It's essentially the implementation of
* QString::replace(const QRegularExpression &, const QString &).
* @see https://github.com/qt/qtbase/blob/97bb0ecfe628b5bb78e798563212adf02129c6f6/src/corelib/text/qstring.cpp#L4594-L4703
*/
QString makeRegexReplacement(QStringView source,
const QRegularExpression &regex,
const QRegularExpressionMatch &match,
const QString &replacement)
{
using SizeType = QString::size_type;
struct QStringCapture {
SizeType pos;
SizeType len;
int captureNumber;
};

qsizetype numCaptures = regex.captureCount();

// 1. build the backreferences list, holding where the backreferences
// are in the replacement string
QVarLengthArray<QStringCapture> backReferences;

SizeType replacementLength = replacement.size();
for (SizeType i = 0; i < replacementLength - 1; i++)
{
if (replacement[i] != u'\\')
{
continue;
}

int no = replacement[i + 1].digitValue();
if (no <= 0 || no > numCaptures)
{
continue;
}

QStringCapture backReference{.pos = i, .len = 2};

if (i < replacementLength - 2)
{
int secondDigit = replacement[i + 2].digitValue();
if (secondDigit != -1 &&
((no * 10) + secondDigit) <= numCaptures)
{
no = (no * 10) + secondDigit;
++backReference.len;
}
}

backReference.captureNumber = no;
backReferences.append(backReference);
}

// 2. iterate on the matches.
// For every match, copy the replacement string in chunks
// with the proper replacements for the backreferences

// length of the new string, with all the replacements
SizeType newLength = 0;
QVarLengthArray<QStringView> chunks;
QStringView replacementView{replacement};

// Initially: empty, as we only care about the replacement
SizeType len = 0;
SizeType lastEnd = 0;
for (const QStringCapture &backReference :
std::as_const(backReferences))
{
// part of "replacement" before the backreference
len = backReference.pos - lastEnd;
if (len > 0)
{
chunks << replacementView.mid(lastEnd, len);
newLength += len;
}

// backreference itself
len = match.capturedLength(backReference.captureNumber);
if (len > 0)
{
chunks << source.mid(
match.capturedStart(backReference.captureNumber), len);
newLength += len;
}

lastEnd = backReference.pos + backReference.len;
}

// add the last part of the replacement string
len = replacementView.size() - lastEnd;
if (len > 0)
{
chunks << replacementView.mid(lastEnd, len);
newLength += len;
}

// 3. assemble the chunks together
QString dst;
dst.reserve(newLength);
for (const QStringView &chunk : std::as_const(chunks))
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 2)
static_assert(sizeof(QChar) == sizeof(decltype(*chunk.utf16())));
dst.append(reinterpret_cast<const QChar *>(chunk.utf16()),
chunk.length());
#else
dst += chunk;
#endif
}
return dst;
}

} // namespace

TwitchMessageBuilder::TwitchMessageBuilder(
Expand Down Expand Up @@ -420,7 +542,9 @@ MessagePtr TwitchMessageBuilder::build()
this->tags, this->originalMessage_, this->messageOffset_);

// This runs through all ignored phrases and runs its replacements on this->originalMessage_
this->runIgnoreReplaces(twitchEmotes);
TwitchMessageBuilder::processIgnorePhrases(
*getSettings()->ignoredMessages.readOnly(), this->originalMessage_,
twitchEmotes);

std::sort(twitchEmotes.begin(), twitchEmotes.end(),
[](const auto &a, const auto &b) {
Expand Down Expand Up @@ -961,12 +1085,12 @@ void TwitchMessageBuilder::appendUsername()
}
}

void TwitchMessageBuilder::runIgnoreReplaces(
void TwitchMessageBuilder::processIgnorePhrases(
const std::vector<IgnorePhrase> &phrases, QString &originalMessage,
std::vector<TwitchEmoteOccurrence> &twitchEmotes)
{
using SizeType = QString::size_type;

auto phrases = getSettings()->ignoredMessages.readOnly();
auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) {
// all emotes outside the range come before `it`
// all emotes in the range start at `it`
Expand Down Expand Up @@ -1035,20 +1159,20 @@ void TwitchMessageBuilder::runIgnoreReplaces(
auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from,
SizeType length, const QString &replacement) {
auto removedEmotes = removeEmotesInRange(from, length);
this->originalMessage_.replace(from, length, replacement);
originalMessage.replace(from, length, replacement);
auto wordStart = from;
while (wordStart > 0)
{
if (this->originalMessage_[wordStart - 1] == ' ')
if (originalMessage[wordStart - 1] == ' ')
{
break;
}
--wordStart;
}
auto wordEnd = from + replacement.length();
while (wordEnd < this->originalMessage_.length())
while (wordEnd < originalMessage.length())
{
if (this->originalMessage_[wordEnd] == ' ')
if (originalMessage[wordEnd] == ' ')
{
break;
}
Expand All @@ -1059,11 +1183,11 @@ void TwitchMessageBuilder::runIgnoreReplaces(
static_cast<int>(replacement.length() - length));

#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
auto midExtendedRef = QStringView{this->originalMessage_}.mid(
wordStart, wordEnd - wordStart);
auto midExtendedRef =
QStringView{originalMessage}.mid(wordStart, wordEnd - wordStart);
#else
auto midExtendedRef =
this->originalMessage_.midRef(wordStart, wordEnd - wordStart);
originalMessage.midRef(wordStart, wordEnd - wordStart);
#endif

for (auto &emote : removedEmotes)
Expand All @@ -1089,7 +1213,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
addReplEmotes(phrase, midExtendedRef, wordStart);
};

for (const auto &phrase : *phrases)
for (const auto &phrase : phrases)
{
if (phrase.isBlock())
{
Expand All @@ -1111,16 +1235,22 @@ void TwitchMessageBuilder::runIgnoreReplaces(
QRegularExpressionMatch match;
size_t iterations = 0;
SizeType from = 0;
while ((from = this->originalMessage_.indexOf(regex, from,
&match)) != -1)
while ((from = originalMessage.indexOf(regex, from, &match)) != -1)
{
auto replacement = phrase.getReplace();
if (regex.captureCount() > 0)
{
replacement = makeRegexReplacement(originalMessage, regex,
match, replacement);
}

replaceMessageAt(phrase, from, match.capturedLength(),
phrase.getReplace());
replacement);
from += phrase.getReplace().length();
iterations++;
if (iterations >= 128)
{
this->originalMessage_ =
originalMessage =
u"Too many replacements - check your ignores!"_s;
return;
}
Expand All @@ -1130,8 +1260,8 @@ void TwitchMessageBuilder::runIgnoreReplaces(
}

SizeType from = 0;
while ((from = this->originalMessage_.indexOf(
pattern, from, phrase.caseSensitivity())) != -1)
while ((from = originalMessage.indexOf(pattern, from,
phrase.caseSensitivity())) != -1)
{
replaceMessageAt(phrase, from, pattern.length(),
phrase.getReplace());
Expand Down
7 changes: 5 additions & 2 deletions src/providers/twitch/TwitchMessageBuilder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ using EmotePtr = std::shared_ptr<const Emote>;
class Channel;
class TwitchChannel;
class MessageThread;
class IgnorePhrase;
struct HelixVip;
using HelixModerator = HelixVip;
struct ChannelPointReward;
Expand Down Expand Up @@ -108,6 +109,10 @@ class TwitchMessageBuilder : public SharedMessageBuilder
const QVariantMap &tags, const QString &originalMessage,
int messageOffset);

static void processIgnorePhrases(
const std::vector<IgnorePhrase> &phrases, QString &originalMessage,
std::vector<TwitchEmoteOccurrence> &twitchEmotes);

private:
void parseUsernameColor() override;
void parseUsername() override;
Expand All @@ -118,8 +123,6 @@ class TwitchMessageBuilder : public SharedMessageBuilder
void parseThread();
void appendUsername();

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

Outcome tryAppendEmote(const EmoteName &name) override;

void addWords(const QStringList &words,
Expand Down
Loading

0 comments on commit b95a6cf

Please sign in to comment.