Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support captures in ignores #5126

Merged
merged 7 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -269,6 +269,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;
pajlada marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -419,7 +541,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 @@ -960,12 +1084,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 @@ -1034,20 +1158,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 @@ -1058,11 +1182,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 @@ -1088,7 +1212,7 @@ void TwitchMessageBuilder::runIgnoreReplaces(
addReplEmotes(phrase, midExtendedRef, wordStart);
};

for (const auto &phrase : *phrases)
for (const auto &phrase : phrases)
{
if (phrase.isBlock())
{
Expand All @@ -1110,16 +1234,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 @@ -1129,8 +1259,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
Loading