refactor(message-builder): move static helper methods to functions (#5652)
This commit is contained in:
@@ -409,6 +409,8 @@ set(SOURCE_FILES
|
||||
providers/twitch/TwitchEmotes.hpp
|
||||
providers/twitch/TwitchHelpers.cpp
|
||||
providers/twitch/TwitchHelpers.hpp
|
||||
providers/twitch/TwitchIrc.cpp
|
||||
providers/twitch/TwitchIrc.hpp
|
||||
providers/twitch/TwitchIrcServer.cpp
|
||||
providers/twitch/TwitchIrcServer.hpp
|
||||
providers/twitch/TwitchUser.cpp
|
||||
|
||||
@@ -1,12 +1,134 @@
|
||||
#include "controllers/ignores/IgnoreController.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Literals.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||
#include "providers/twitch/TwitchAccount.hpp"
|
||||
#include "providers/twitch/TwitchIrc.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino::literals;
|
||||
|
||||
/**
|
||||
* 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 ®ex,
|
||||
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))
|
||||
{
|
||||
dst += chunk;
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
|
||||
@@ -65,4 +187,187 @@ bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
|
||||
return false;
|
||||
}
|
||||
|
||||
void processIgnorePhrases(const std::vector<IgnorePhrase> &phrases,
|
||||
QString &content,
|
||||
std::vector<TwitchEmoteOccurrence> &twitchEmotes)
|
||||
{
|
||||
using SizeType = QString::size_type;
|
||||
|
||||
auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) {
|
||||
// all emotes outside the range come before `it`
|
||||
// all emotes in the range start at `it`
|
||||
auto it = std::partition(
|
||||
twitchEmotes.begin(), twitchEmotes.end(),
|
||||
[pos, len](const auto &item) {
|
||||
// returns true for emotes outside the range
|
||||
return !((item.start >= pos) && item.start < (pos + len));
|
||||
});
|
||||
std::vector<TwitchEmoteOccurrence> emotesInRange(it,
|
||||
twitchEmotes.end());
|
||||
twitchEmotes.erase(it, twitchEmotes.end());
|
||||
return emotesInRange;
|
||||
};
|
||||
|
||||
auto shiftIndicesAfter = [&twitchEmotes](int pos, int by) {
|
||||
for (auto &item : twitchEmotes)
|
||||
{
|
||||
auto &index = item.start;
|
||||
if (index >= pos)
|
||||
{
|
||||
index += by;
|
||||
item.end += by;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto addReplEmotes = [&twitchEmotes](const IgnorePhrase &phrase,
|
||||
const auto &midrepl,
|
||||
SizeType startIndex) {
|
||||
if (!phrase.containsEmote())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
auto words = midrepl.tokenize(u' ');
|
||||
#else
|
||||
auto words = midrepl.split(' ');
|
||||
#endif
|
||||
SizeType pos = 0;
|
||||
for (const auto &word : words)
|
||||
{
|
||||
for (const auto &emote : phrase.getEmotes())
|
||||
{
|
||||
if (word == emote.first.string)
|
||||
{
|
||||
if (emote.second == nullptr)
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "emote null" << emote.first.string;
|
||||
}
|
||||
twitchEmotes.push_back(TwitchEmoteOccurrence{
|
||||
static_cast<int>(startIndex + pos),
|
||||
static_cast<int>(startIndex + pos +
|
||||
emote.first.string.length()),
|
||||
emote.second,
|
||||
emote.first,
|
||||
});
|
||||
}
|
||||
}
|
||||
pos += word.length() + 1;
|
||||
}
|
||||
};
|
||||
|
||||
auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from,
|
||||
SizeType length, const QString &replacement) {
|
||||
auto removedEmotes = removeEmotesInRange(from, length);
|
||||
content.replace(from, length, replacement);
|
||||
auto wordStart = from;
|
||||
while (wordStart > 0)
|
||||
{
|
||||
if (content[wordStart - 1] == ' ')
|
||||
{
|
||||
break;
|
||||
}
|
||||
--wordStart;
|
||||
}
|
||||
auto wordEnd = from + replacement.length();
|
||||
while (wordEnd < content.length())
|
||||
{
|
||||
if (content[wordEnd] == ' ')
|
||||
{
|
||||
break;
|
||||
}
|
||||
++wordEnd;
|
||||
}
|
||||
|
||||
shiftIndicesAfter(static_cast<int>(from + length),
|
||||
static_cast<int>(replacement.length() - length));
|
||||
|
||||
auto midExtendedRef =
|
||||
QStringView{content}.mid(wordStart, wordEnd - wordStart);
|
||||
|
||||
for (auto &emote : removedEmotes)
|
||||
{
|
||||
if (emote.ptr == nullptr)
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "Invalid emote occurrence" << emote.name.string;
|
||||
continue;
|
||||
}
|
||||
QRegularExpression emoteregex(
|
||||
"\\b" + emote.name.string + "\\b",
|
||||
QRegularExpression::UseUnicodePropertiesOption);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
auto match = emoteregex.matchView(midExtendedRef);
|
||||
#else
|
||||
auto match = emoteregex.match(midExtendedRef);
|
||||
#endif
|
||||
if (match.hasMatch())
|
||||
{
|
||||
emote.start = static_cast<int>(from + match.capturedStart());
|
||||
emote.end = static_cast<int>(from + match.capturedEnd());
|
||||
twitchEmotes.push_back(std::move(emote));
|
||||
}
|
||||
}
|
||||
|
||||
addReplEmotes(phrase, midExtendedRef, wordStart);
|
||||
};
|
||||
|
||||
for (const auto &phrase : phrases)
|
||||
{
|
||||
if (phrase.isBlock())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const auto &pattern = phrase.getPattern();
|
||||
if (pattern.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (phrase.isRegex())
|
||||
{
|
||||
const auto ®ex = phrase.getRegex();
|
||||
if (!regex.isValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QRegularExpressionMatch match;
|
||||
size_t iterations = 0;
|
||||
SizeType from = 0;
|
||||
while ((from = content.indexOf(regex, from, &match)) != -1)
|
||||
{
|
||||
auto replacement = phrase.getReplace();
|
||||
if (regex.captureCount() > 0)
|
||||
{
|
||||
replacement = makeRegexReplacement(content, regex, match,
|
||||
replacement);
|
||||
}
|
||||
|
||||
replaceMessageAt(phrase, from, match.capturedLength(),
|
||||
replacement);
|
||||
from += phrase.getReplace().length();
|
||||
iterations++;
|
||||
if (iterations >= 128)
|
||||
{
|
||||
content = u"Too many replacements - check your ignores!"_s;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
SizeType from = 0;
|
||||
while ((from = content.indexOf(pattern, from,
|
||||
phrase.caseSensitivity())) != -1)
|
||||
{
|
||||
replaceMessageAt(phrase, from, pattern.length(),
|
||||
phrase.getReplace());
|
||||
from += phrase.getReplace().length();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
#include <QString>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class IgnorePhrase;
|
||||
struct TwitchEmoteOccurrence;
|
||||
|
||||
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
|
||||
|
||||
struct IgnoredMessageParameters {
|
||||
@@ -16,4 +21,17 @@ struct IgnoredMessageParameters {
|
||||
|
||||
bool isIgnoredMessage(IgnoredMessageParameters &¶ms);
|
||||
|
||||
/// @brief Processes replacement ignore-phrases for a message
|
||||
///
|
||||
/// @param phrases A list of IgnorePhrases to process. Block phrases as well as
|
||||
/// invalid phrases are ignored.
|
||||
/// @param content The message text. This gets altered by replacements.
|
||||
/// @param twitchEmotes A list of emotes present in the message. Occurrences
|
||||
/// that have been removed from the message will also be
|
||||
/// removed in this list. Similarly, if new emotes are added
|
||||
/// from a replacement, this list gets updated as well.
|
||||
void processIgnorePhrases(const std::vector<IgnorePhrase> &phrases,
|
||||
QString &content,
|
||||
std::vector<TwitchEmoteOccurrence> &twitchEmotes);
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "providers/twitch/TwitchBadge.hpp"
|
||||
#include "providers/twitch/TwitchBadges.hpp"
|
||||
#include "providers/twitch/TwitchChannel.hpp"
|
||||
#include "providers/twitch/TwitchIrc.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "singletons/Resources.hpp"
|
||||
@@ -237,77 +238,6 @@ QString stylizeUsername(const QString &username, const Message &message)
|
||||
return usernameText;
|
||||
}
|
||||
|
||||
void appendTwitchEmoteOccurrences(const QString &emote,
|
||||
std::vector<TwitchEmoteOccurrence> &vec,
|
||||
const std::vector<int> &correctPositions,
|
||||
const QString &originalMessage,
|
||||
int messageOffset)
|
||||
{
|
||||
auto *app = getApp();
|
||||
if (!emote.contains(':'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto parameters = emote.split(':');
|
||||
|
||||
if (parameters.length() < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto id = EmoteId{parameters.at(0)};
|
||||
|
||||
auto occurrences = parameters.at(1).split(',');
|
||||
|
||||
for (const QString &occurrence : occurrences)
|
||||
{
|
||||
auto coords = occurrence.split('-');
|
||||
|
||||
if (coords.length() < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto from = coords.at(0).toUInt() - messageOffset;
|
||||
auto to = coords.at(1).toUInt() - messageOffset;
|
||||
auto maxPositions = correctPositions.size();
|
||||
if (from > to || to >= maxPositions)
|
||||
{
|
||||
// Emote coords are out of range
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "Emote coords" << from << "-" << to << "are out of range ("
|
||||
<< maxPositions << ")";
|
||||
return;
|
||||
}
|
||||
|
||||
auto start = correctPositions[from];
|
||||
auto end = correctPositions[to];
|
||||
if (start > end || start < 0 || end > originalMessage.length())
|
||||
{
|
||||
// Emote coords are out of range from the modified character positions
|
||||
qCDebug(chatterinoTwitch) << "Emote coords" << from << "-" << to
|
||||
<< "are out of range after offsets ("
|
||||
<< originalMessage.length() << ")";
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = EmoteName{originalMessage.mid(start, end - start + 1)};
|
||||
TwitchEmoteOccurrence emoteOccurrence{
|
||||
start,
|
||||
end,
|
||||
app->getEmotes()->getTwitchEmotes()->getOrCreateEmote(id, name),
|
||||
name,
|
||||
};
|
||||
if (emoteOccurrence.ptr == nullptr)
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "nullptr" << emoteOccurrence.name.string;
|
||||
}
|
||||
vec.push_back(std::move(emoteOccurrence));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<EmotePtr> getTwitchBadge(const Badge &badge,
|
||||
const TwitchChannel *twitchChannel)
|
||||
{
|
||||
@@ -420,120 +350,6 @@ void appendBadges(MessageBuilder *builder, const std::vector<Badge> &badges,
|
||||
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 ®ex,
|
||||
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))
|
||||
{
|
||||
dst += chunk;
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
bool doesWordContainATwitchEmote(
|
||||
int cursor, const QString &word,
|
||||
const std::vector<TwitchEmoteOccurrence> &twitchEmotes,
|
||||
@@ -1358,13 +1174,12 @@ MessagePtr MessageBuilder::build()
|
||||
}
|
||||
|
||||
// Twitch emotes
|
||||
auto twitchEmotes = MessageBuilder::parseTwitchEmotes(
|
||||
this->tags, this->originalMessage_, this->messageOffset_);
|
||||
auto twitchEmotes = parseTwitchEmotes(this->tags, this->originalMessage_,
|
||||
this->messageOffset_);
|
||||
|
||||
// This runs through all ignored phrases and runs its replacements on this->originalMessage_
|
||||
MessageBuilder::processIgnorePhrases(
|
||||
*getSettings()->ignoredMessages.readOnly(), this->originalMessage_,
|
||||
twitchEmotes);
|
||||
processIgnorePhrases(*getSettings()->ignoredMessages.readOnly(),
|
||||
this->originalMessage_, twitchEmotes);
|
||||
|
||||
std::sort(twitchEmotes.begin(), twitchEmotes.end(),
|
||||
[](const auto &a, const auto &b) {
|
||||
@@ -2178,268 +1993,6 @@ MessagePtr MessageBuilder::makeLowTrustUpdateMessage(
|
||||
return builder.release();
|
||||
}
|
||||
|
||||
std::unordered_map<QString, QString> MessageBuilder::parseBadgeInfoTag(
|
||||
const QVariantMap &tags)
|
||||
{
|
||||
std::unordered_map<QString, QString> infoMap;
|
||||
|
||||
auto infoIt = tags.constFind("badge-info");
|
||||
if (infoIt == tags.end())
|
||||
{
|
||||
return infoMap;
|
||||
}
|
||||
|
||||
auto info = infoIt.value().toString().split(',', Qt::SkipEmptyParts);
|
||||
|
||||
for (const QString &badge : info)
|
||||
{
|
||||
infoMap.emplace(slashKeyValue(badge));
|
||||
}
|
||||
|
||||
return infoMap;
|
||||
}
|
||||
|
||||
std::vector<Badge> MessageBuilder::parseBadgeTag(const QVariantMap &tags)
|
||||
{
|
||||
std::vector<Badge> b;
|
||||
|
||||
auto badgesIt = tags.constFind("badges");
|
||||
if (badgesIt == tags.end())
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
auto badges = badgesIt.value().toString().split(',', Qt::SkipEmptyParts);
|
||||
|
||||
for (const QString &badge : badges)
|
||||
{
|
||||
if (!badge.contains('/'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto pair = slashKeyValue(badge);
|
||||
b.emplace_back(Badge{pair.first, pair.second});
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
std::vector<TwitchEmoteOccurrence> MessageBuilder::parseTwitchEmotes(
|
||||
const QVariantMap &tags, const QString &originalMessage, int messageOffset)
|
||||
{
|
||||
// Twitch emotes
|
||||
std::vector<TwitchEmoteOccurrence> twitchEmotes;
|
||||
|
||||
auto emotesTag = tags.find("emotes");
|
||||
|
||||
if (emotesTag == tags.end())
|
||||
{
|
||||
return twitchEmotes;
|
||||
}
|
||||
|
||||
QStringList emoteString = emotesTag.value().toString().split('/');
|
||||
std::vector<int> correctPositions;
|
||||
for (int i = 0; i < originalMessage.size(); ++i)
|
||||
{
|
||||
if (!originalMessage.at(i).isLowSurrogate())
|
||||
{
|
||||
correctPositions.push_back(i);
|
||||
}
|
||||
}
|
||||
for (const QString &emote : emoteString)
|
||||
{
|
||||
appendTwitchEmoteOccurrences(emote, twitchEmotes, correctPositions,
|
||||
originalMessage, messageOffset);
|
||||
}
|
||||
|
||||
return twitchEmotes;
|
||||
}
|
||||
|
||||
void MessageBuilder::processIgnorePhrases(
|
||||
const std::vector<IgnorePhrase> &phrases, QString &originalMessage,
|
||||
std::vector<TwitchEmoteOccurrence> &twitchEmotes)
|
||||
{
|
||||
using SizeType = QString::size_type;
|
||||
|
||||
auto removeEmotesInRange = [&twitchEmotes](SizeType pos, SizeType len) {
|
||||
// all emotes outside the range come before `it`
|
||||
// all emotes in the range start at `it`
|
||||
auto it = std::partition(
|
||||
twitchEmotes.begin(), twitchEmotes.end(),
|
||||
[pos, len](const auto &item) {
|
||||
// returns true for emotes outside the range
|
||||
return !((item.start >= pos) && item.start < (pos + len));
|
||||
});
|
||||
std::vector<TwitchEmoteOccurrence> emotesInRange(it,
|
||||
twitchEmotes.end());
|
||||
twitchEmotes.erase(it, twitchEmotes.end());
|
||||
return emotesInRange;
|
||||
};
|
||||
|
||||
auto shiftIndicesAfter = [&twitchEmotes](int pos, int by) {
|
||||
for (auto &item : twitchEmotes)
|
||||
{
|
||||
auto &index = item.start;
|
||||
if (index >= pos)
|
||||
{
|
||||
index += by;
|
||||
item.end += by;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto addReplEmotes = [&twitchEmotes](const IgnorePhrase &phrase,
|
||||
const auto &midrepl,
|
||||
SizeType startIndex) {
|
||||
if (!phrase.containsEmote())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
auto words = midrepl.tokenize(u' ');
|
||||
#else
|
||||
auto words = midrepl.split(' ');
|
||||
#endif
|
||||
SizeType pos = 0;
|
||||
for (const auto &word : words)
|
||||
{
|
||||
for (const auto &emote : phrase.getEmotes())
|
||||
{
|
||||
if (word == emote.first.string)
|
||||
{
|
||||
if (emote.second == nullptr)
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "emote null" << emote.first.string;
|
||||
}
|
||||
twitchEmotes.push_back(TwitchEmoteOccurrence{
|
||||
static_cast<int>(startIndex + pos),
|
||||
static_cast<int>(startIndex + pos +
|
||||
emote.first.string.length()),
|
||||
emote.second,
|
||||
emote.first,
|
||||
});
|
||||
}
|
||||
}
|
||||
pos += word.length() + 1;
|
||||
}
|
||||
};
|
||||
|
||||
auto replaceMessageAt = [&](const IgnorePhrase &phrase, SizeType from,
|
||||
SizeType length, const QString &replacement) {
|
||||
auto removedEmotes = removeEmotesInRange(from, length);
|
||||
originalMessage.replace(from, length, replacement);
|
||||
auto wordStart = from;
|
||||
while (wordStart > 0)
|
||||
{
|
||||
if (originalMessage[wordStart - 1] == ' ')
|
||||
{
|
||||
break;
|
||||
}
|
||||
--wordStart;
|
||||
}
|
||||
auto wordEnd = from + replacement.length();
|
||||
while (wordEnd < originalMessage.length())
|
||||
{
|
||||
if (originalMessage[wordEnd] == ' ')
|
||||
{
|
||||
break;
|
||||
}
|
||||
++wordEnd;
|
||||
}
|
||||
|
||||
shiftIndicesAfter(static_cast<int>(from + length),
|
||||
static_cast<int>(replacement.length() - length));
|
||||
|
||||
auto midExtendedRef =
|
||||
QStringView{originalMessage}.mid(wordStart, wordEnd - wordStart);
|
||||
|
||||
for (auto &emote : removedEmotes)
|
||||
{
|
||||
if (emote.ptr == nullptr)
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "Invalid emote occurrence" << emote.name.string;
|
||||
continue;
|
||||
}
|
||||
QRegularExpression emoteregex(
|
||||
"\\b" + emote.name.string + "\\b",
|
||||
QRegularExpression::UseUnicodePropertiesOption);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
auto match = emoteregex.matchView(midExtendedRef);
|
||||
#else
|
||||
auto match = emoteregex.match(midExtendedRef);
|
||||
#endif
|
||||
if (match.hasMatch())
|
||||
{
|
||||
emote.start = static_cast<int>(from + match.capturedStart());
|
||||
emote.end = static_cast<int>(from + match.capturedEnd());
|
||||
twitchEmotes.push_back(std::move(emote));
|
||||
}
|
||||
}
|
||||
|
||||
addReplEmotes(phrase, midExtendedRef, wordStart);
|
||||
};
|
||||
|
||||
for (const auto &phrase : phrases)
|
||||
{
|
||||
if (phrase.isBlock())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const auto &pattern = phrase.getPattern();
|
||||
if (pattern.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (phrase.isRegex())
|
||||
{
|
||||
const auto ®ex = phrase.getRegex();
|
||||
if (!regex.isValid())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QRegularExpressionMatch match;
|
||||
size_t iterations = 0;
|
||||
SizeType from = 0;
|
||||
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(),
|
||||
replacement);
|
||||
from += phrase.getReplace().length();
|
||||
iterations++;
|
||||
if (iterations >= 128)
|
||||
{
|
||||
originalMessage =
|
||||
u"Too many replacements - check your ignores!"_s;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
SizeType from = 0;
|
||||
while ((from = originalMessage.indexOf(pattern, from,
|
||||
phrase.caseSensitivity())) != -1)
|
||||
{
|
||||
replaceMessageAt(phrase, from, pattern.length(),
|
||||
phrase.getReplace());
|
||||
from += phrase.getReplace().length();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MessageBuilder::addTextOrEmoji(EmotePtr emote)
|
||||
{
|
||||
this->emplace<EmoteElement>(emote, MessageElementFlag::EmojiAll);
|
||||
@@ -3159,7 +2712,7 @@ void MessageBuilder::appendTwitchBadges()
|
||||
return;
|
||||
}
|
||||
|
||||
auto badgeInfos = MessageBuilder::parseBadgeInfoTag(this->tags);
|
||||
auto badgeInfos = parseBadgeInfoTag(this->tags);
|
||||
auto badges = parseBadgeTag(this->tags);
|
||||
appendBadges(this, badges, badgeInfos, this->twitchChannel);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ struct HelixVip;
|
||||
using HelixModerator = HelixVip;
|
||||
struct ChannelPointReward;
|
||||
struct DeleteAction;
|
||||
struct TwitchEmoteOccurrence;
|
||||
|
||||
namespace linkparser {
|
||||
struct Parsed;
|
||||
@@ -89,19 +90,6 @@ struct MessageParseArgs {
|
||||
QString channelPointRewardId = "";
|
||||
};
|
||||
|
||||
struct TwitchEmoteOccurrence {
|
||||
int start;
|
||||
int end;
|
||||
EmotePtr ptr;
|
||||
EmoteName name;
|
||||
|
||||
bool operator==(const TwitchEmoteOccurrence &other) const
|
||||
{
|
||||
return std::tie(this->start, this->end, this->ptr, this->name) ==
|
||||
std::tie(other.start, other.end, other.ptr, other.name);
|
||||
}
|
||||
};
|
||||
|
||||
class MessageBuilder
|
||||
{
|
||||
public:
|
||||
@@ -237,20 +225,6 @@ public:
|
||||
static MessagePtr makeLowTrustUpdateMessage(
|
||||
const PubSubLowTrustUsersMessage &action);
|
||||
|
||||
static std::unordered_map<QString, QString> parseBadgeInfoTag(
|
||||
const QVariantMap &tags);
|
||||
|
||||
// Parses "badges" tag which contains a comma separated list of key-value elements
|
||||
static std::vector<Badge> parseBadgeTag(const QVariantMap &tags);
|
||||
|
||||
static std::vector<TwitchEmoteOccurrence> parseTwitchEmotes(
|
||||
const QVariantMap &tags, const QString &originalMessage,
|
||||
int messageOffset);
|
||||
|
||||
static void processIgnorePhrases(
|
||||
const std::vector<IgnorePhrase> &phrases, QString &originalMessage,
|
||||
std::vector<TwitchEmoteOccurrence> &twitchEmotes);
|
||||
|
||||
protected:
|
||||
void addTextOrEmoji(EmotePtr emote);
|
||||
void addTextOrEmoji(const QString &string_);
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
#include "providers/twitch/TwitchIrc.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/Aliases.hpp"
|
||||
#include "common/QLogging.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
#include "util/IrcHelpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
|
||||
void appendTwitchEmoteOccurrences(const QString &emote,
|
||||
std::vector<TwitchEmoteOccurrence> &vec,
|
||||
const std::vector<int> &correctPositions,
|
||||
const QString &originalMessage,
|
||||
int messageOffset)
|
||||
{
|
||||
auto *app = getApp();
|
||||
if (!emote.contains(':'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto parameters = emote.split(':');
|
||||
|
||||
if (parameters.length() < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto id = EmoteId{parameters.at(0)};
|
||||
|
||||
auto occurrences = parameters.at(1).split(',');
|
||||
|
||||
for (const QString &occurrence : occurrences)
|
||||
{
|
||||
auto coords = occurrence.split('-');
|
||||
|
||||
if (coords.length() < 2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto from = coords.at(0).toUInt() - messageOffset;
|
||||
auto to = coords.at(1).toUInt() - messageOffset;
|
||||
auto maxPositions = correctPositions.size();
|
||||
if (from > to || to >= maxPositions)
|
||||
{
|
||||
// Emote coords are out of range
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "Emote coords" << from << "-" << to << "are out of range ("
|
||||
<< maxPositions << ")";
|
||||
return;
|
||||
}
|
||||
|
||||
auto start = correctPositions[from];
|
||||
auto end = correctPositions[to];
|
||||
if (start > end || start < 0 || end > originalMessage.length())
|
||||
{
|
||||
// Emote coords are out of range from the modified character positions
|
||||
qCDebug(chatterinoTwitch) << "Emote coords" << from << "-" << to
|
||||
<< "are out of range after offsets ("
|
||||
<< originalMessage.length() << ")";
|
||||
return;
|
||||
}
|
||||
|
||||
auto name = EmoteName{originalMessage.mid(start, end - start + 1)};
|
||||
TwitchEmoteOccurrence emoteOccurrence{
|
||||
start,
|
||||
end,
|
||||
app->getEmotes()->getTwitchEmotes()->getOrCreateEmote(id, name),
|
||||
name,
|
||||
};
|
||||
if (emoteOccurrence.ptr == nullptr)
|
||||
{
|
||||
qCDebug(chatterinoTwitch)
|
||||
<< "nullptr" << emoteOccurrence.name.string;
|
||||
}
|
||||
vec.push_back(std::move(emoteOccurrence));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
std::unordered_map<QString, QString> parseBadgeInfoTag(const QVariantMap &tags)
|
||||
{
|
||||
std::unordered_map<QString, QString> infoMap;
|
||||
|
||||
auto infoIt = tags.constFind("badge-info");
|
||||
if (infoIt == tags.end())
|
||||
{
|
||||
return infoMap;
|
||||
}
|
||||
|
||||
auto info = infoIt.value().toString().split(',', Qt::SkipEmptyParts);
|
||||
|
||||
for (const QString &badge : info)
|
||||
{
|
||||
infoMap.emplace(slashKeyValue(badge));
|
||||
}
|
||||
|
||||
return infoMap;
|
||||
}
|
||||
|
||||
std::vector<Badge> parseBadgeTag(const QVariantMap &tags)
|
||||
{
|
||||
std::vector<Badge> b;
|
||||
|
||||
auto badgesIt = tags.constFind("badges");
|
||||
if (badgesIt == tags.end())
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
auto badges = badgesIt.value().toString().split(',', Qt::SkipEmptyParts);
|
||||
|
||||
for (const QString &badge : badges)
|
||||
{
|
||||
if (!badge.contains('/'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
auto pair = slashKeyValue(badge);
|
||||
b.emplace_back(Badge{pair.first, pair.second});
|
||||
}
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
std::vector<TwitchEmoteOccurrence> parseTwitchEmotes(const QVariantMap &tags,
|
||||
const QString &content,
|
||||
int messageOffset)
|
||||
{
|
||||
// Twitch emotes
|
||||
std::vector<TwitchEmoteOccurrence> twitchEmotes;
|
||||
|
||||
auto emotesTag = tags.find("emotes");
|
||||
|
||||
if (emotesTag == tags.end())
|
||||
{
|
||||
return twitchEmotes;
|
||||
}
|
||||
|
||||
QStringList emoteString = emotesTag.value().toString().split('/');
|
||||
std::vector<int> correctPositions;
|
||||
for (int i = 0; i < content.size(); ++i)
|
||||
{
|
||||
if (!content.at(i).isLowSurrogate())
|
||||
{
|
||||
correctPositions.push_back(i);
|
||||
}
|
||||
}
|
||||
for (const QString &emote : emoteString)
|
||||
{
|
||||
appendTwitchEmoteOccurrences(emote, twitchEmotes, correctPositions,
|
||||
content, messageOffset);
|
||||
}
|
||||
|
||||
return twitchEmotes;
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include "messages/Emote.hpp"
|
||||
#include "providers/twitch/TwitchBadge.hpp"
|
||||
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
struct TwitchEmoteOccurrence {
|
||||
int start;
|
||||
int end;
|
||||
EmotePtr ptr;
|
||||
EmoteName name;
|
||||
|
||||
bool operator==(const TwitchEmoteOccurrence &other) const
|
||||
{
|
||||
return std::tie(this->start, this->end, this->ptr, this->name) ==
|
||||
std::tie(other.start, other.end, other.ptr, other.name);
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Parses the `badge-info` tag of an IRC message
|
||||
///
|
||||
/// The `badge-info` tag maps badge-names to a value. Subscriber badges, for
|
||||
/// example, are mapped to the number of months the chatter is subscribed for.
|
||||
///
|
||||
/// **Example**:
|
||||
/// `badge-info=subscriber/22` would be parsed as `{ subscriber => 22 }`
|
||||
///
|
||||
/// @param tags The tags of the IRC message
|
||||
/// @returns A map of badge-names to their values
|
||||
std::unordered_map<QString, QString> parseBadgeInfoTag(const QVariantMap &tags);
|
||||
|
||||
/// @brief Parses the `badges` tag of an IRC message
|
||||
///
|
||||
/// The `badges` tag contains a comma separated list of key-value elements which
|
||||
/// make up the name and version of each badge.
|
||||
///
|
||||
/// **Example**:
|
||||
/// `badges=broadcaster/1,subscriber/18` would be parsed as
|
||||
/// `[(broadcaster, 1), (subscriber, 18)]`
|
||||
///
|
||||
/// @param tags The tags of the IRC message
|
||||
/// @returns A list of badges (name and version)
|
||||
std::vector<Badge> parseBadgeTag(const QVariantMap &tags);
|
||||
|
||||
/// @brief Parses Twitch emotes in an IRC message
|
||||
///
|
||||
/// @param tags The tags of the IRC message
|
||||
/// @param content The message text. This might be shortened due to skipping
|
||||
/// content at the start. `messageOffset` describes this offset.
|
||||
/// @param messageOffset The offset of `content` compared to the original
|
||||
/// message text. Used for calculating indices into the
|
||||
/// message. An offset of 3, for example, indicates that
|
||||
/// `content` excludes the first three characters of the
|
||||
/// original message (`@a foo` (original message) -> `foo`
|
||||
/// (content)).
|
||||
/// @returns A list of emotes and their positions
|
||||
std::vector<TwitchEmoteOccurrence> parseTwitchEmotes(const QVariantMap &tags,
|
||||
const QString &content,
|
||||
int messageOffset);
|
||||
|
||||
} // namespace chatterino
|
||||
Reference in New Issue
Block a user