mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
refactor(message-builder): move static helper methods to functions (#5652)
This commit is contained in:
parent
6d139af553
commit
800f6df2cf
|
@ -107,6 +107,7 @@
|
||||||
- Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607)
|
- Dev: Twitch messages are now sent using Twitch's Helix API instead of IRC by default. (#5607)
|
||||||
- Dev: `GIFTimer` is no longer initialized in tests. (#5608)
|
- Dev: `GIFTimer` is no longer initialized in tests. (#5608)
|
||||||
- Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616)
|
- Dev: Emojis now use flags instead of a set of strings for capabilities. (#5616)
|
||||||
|
- Dev: Refactored static `MessageBuilder` helpers to standalone functions. (#5652)
|
||||||
|
|
||||||
## 2.5.1
|
## 2.5.1
|
||||||
|
|
||||||
|
|
|
@ -409,6 +409,8 @@ set(SOURCE_FILES
|
||||||
providers/twitch/TwitchEmotes.hpp
|
providers/twitch/TwitchEmotes.hpp
|
||||||
providers/twitch/TwitchHelpers.cpp
|
providers/twitch/TwitchHelpers.cpp
|
||||||
providers/twitch/TwitchHelpers.hpp
|
providers/twitch/TwitchHelpers.hpp
|
||||||
|
providers/twitch/TwitchIrc.cpp
|
||||||
|
providers/twitch/TwitchIrc.hpp
|
||||||
providers/twitch/TwitchIrcServer.cpp
|
providers/twitch/TwitchIrcServer.cpp
|
||||||
providers/twitch/TwitchIrcServer.hpp
|
providers/twitch/TwitchIrcServer.hpp
|
||||||
providers/twitch/TwitchUser.cpp
|
providers/twitch/TwitchUser.cpp
|
||||||
|
|
|
@ -1,12 +1,134 @@
|
||||||
#include "controllers/ignores/IgnoreController.hpp"
|
#include "controllers/ignores/IgnoreController.hpp"
|
||||||
|
|
||||||
#include "Application.hpp"
|
#include "Application.hpp"
|
||||||
|
#include "common/Literals.hpp"
|
||||||
#include "common/QLogging.hpp"
|
#include "common/QLogging.hpp"
|
||||||
#include "controllers/accounts/AccountController.hpp"
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||||
#include "providers/twitch/TwitchAccount.hpp"
|
#include "providers/twitch/TwitchAccount.hpp"
|
||||||
|
#include "providers/twitch/TwitchIrc.hpp"
|
||||||
#include "singletons/Settings.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 {
|
namespace chatterino {
|
||||||
|
|
||||||
bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
|
bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
|
||||||
|
@ -65,4 +187,187 @@ bool isIgnoredMessage(IgnoredMessageParameters &¶ms)
|
||||||
return false;
|
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
|
} // namespace chatterino
|
||||||
|
|
|
@ -2,8 +2,13 @@
|
||||||
|
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
|
class IgnorePhrase;
|
||||||
|
struct TwitchEmoteOccurrence;
|
||||||
|
|
||||||
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
|
enum class ShowIgnoredUsersMessages { Never, IfModerator, IfBroadcaster };
|
||||||
|
|
||||||
struct IgnoredMessageParameters {
|
struct IgnoredMessageParameters {
|
||||||
|
@ -16,4 +21,17 @@ struct IgnoredMessageParameters {
|
||||||
|
|
||||||
bool isIgnoredMessage(IgnoredMessageParameters &¶ms);
|
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
|
} // namespace chatterino
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
#include "providers/twitch/TwitchBadge.hpp"
|
#include "providers/twitch/TwitchBadge.hpp"
|
||||||
#include "providers/twitch/TwitchBadges.hpp"
|
#include "providers/twitch/TwitchBadges.hpp"
|
||||||
#include "providers/twitch/TwitchChannel.hpp"
|
#include "providers/twitch/TwitchChannel.hpp"
|
||||||
|
#include "providers/twitch/TwitchIrc.hpp"
|
||||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||||
#include "singletons/Emotes.hpp"
|
#include "singletons/Emotes.hpp"
|
||||||
#include "singletons/Resources.hpp"
|
#include "singletons/Resources.hpp"
|
||||||
|
@ -237,77 +238,6 @@ QString stylizeUsername(const QString &username, const Message &message)
|
||||||
return usernameText;
|
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,
|
std::optional<EmotePtr> getTwitchBadge(const Badge &badge,
|
||||||
const TwitchChannel *twitchChannel)
|
const TwitchChannel *twitchChannel)
|
||||||
{
|
{
|
||||||
|
@ -420,120 +350,6 @@ void appendBadges(MessageBuilder *builder, const std::vector<Badge> &badges,
|
||||||
builder->message().badgeInfos = badgeInfos;
|
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(
|
bool doesWordContainATwitchEmote(
|
||||||
int cursor, const QString &word,
|
int cursor, const QString &word,
|
||||||
const std::vector<TwitchEmoteOccurrence> &twitchEmotes,
|
const std::vector<TwitchEmoteOccurrence> &twitchEmotes,
|
||||||
|
@ -1358,13 +1174,12 @@ MessagePtr MessageBuilder::build()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Twitch emotes
|
// Twitch emotes
|
||||||
auto twitchEmotes = MessageBuilder::parseTwitchEmotes(
|
auto twitchEmotes = parseTwitchEmotes(this->tags, this->originalMessage_,
|
||||||
this->tags, this->originalMessage_, this->messageOffset_);
|
this->messageOffset_);
|
||||||
|
|
||||||
// This runs through all ignored phrases and runs its replacements on this->originalMessage_
|
// This runs through all ignored phrases and runs its replacements on this->originalMessage_
|
||||||
MessageBuilder::processIgnorePhrases(
|
processIgnorePhrases(*getSettings()->ignoredMessages.readOnly(),
|
||||||
*getSettings()->ignoredMessages.readOnly(), this->originalMessage_,
|
this->originalMessage_, twitchEmotes);
|
||||||
twitchEmotes);
|
|
||||||
|
|
||||||
std::sort(twitchEmotes.begin(), twitchEmotes.end(),
|
std::sort(twitchEmotes.begin(), twitchEmotes.end(),
|
||||||
[](const auto &a, const auto &b) {
|
[](const auto &a, const auto &b) {
|
||||||
|
@ -2178,268 +1993,6 @@ MessagePtr MessageBuilder::makeLowTrustUpdateMessage(
|
||||||
return builder.release();
|
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)
|
void MessageBuilder::addTextOrEmoji(EmotePtr emote)
|
||||||
{
|
{
|
||||||
this->emplace<EmoteElement>(emote, MessageElementFlag::EmojiAll);
|
this->emplace<EmoteElement>(emote, MessageElementFlag::EmojiAll);
|
||||||
|
@ -3159,7 +2712,7 @@ void MessageBuilder::appendTwitchBadges()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto badgeInfos = MessageBuilder::parseBadgeInfoTag(this->tags);
|
auto badgeInfos = parseBadgeInfoTag(this->tags);
|
||||||
auto badges = parseBadgeTag(this->tags);
|
auto badges = parseBadgeTag(this->tags);
|
||||||
appendBadges(this, badges, badgeInfos, this->twitchChannel);
|
appendBadges(this, badges, badgeInfos, this->twitchChannel);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ struct HelixVip;
|
||||||
using HelixModerator = HelixVip;
|
using HelixModerator = HelixVip;
|
||||||
struct ChannelPointReward;
|
struct ChannelPointReward;
|
||||||
struct DeleteAction;
|
struct DeleteAction;
|
||||||
|
struct TwitchEmoteOccurrence;
|
||||||
|
|
||||||
namespace linkparser {
|
namespace linkparser {
|
||||||
struct Parsed;
|
struct Parsed;
|
||||||
|
@ -89,19 +90,6 @@ struct MessageParseArgs {
|
||||||
QString channelPointRewardId = "";
|
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
|
class MessageBuilder
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
@ -237,20 +225,6 @@ public:
|
||||||
static MessagePtr makeLowTrustUpdateMessage(
|
static MessagePtr makeLowTrustUpdateMessage(
|
||||||
const PubSubLowTrustUsersMessage &action);
|
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:
|
protected:
|
||||||
void addTextOrEmoji(EmotePtr emote);
|
void addTextOrEmoji(EmotePtr emote);
|
||||||
void addTextOrEmoji(const QString &string_);
|
void addTextOrEmoji(const QString &string_);
|
||||||
|
|
166
src/providers/twitch/TwitchIrc.cpp
Normal file
166
src/providers/twitch/TwitchIrc.cpp
Normal file
|
@ -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
|
67
src/providers/twitch/TwitchIrc.hpp
Normal file
67
src/providers/twitch/TwitchIrc.hpp
Normal file
|
@ -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
|
|
@ -48,6 +48,8 @@ set(test_SOURCES
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/FlagsEnum.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/MessageLayoutContainer.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/CancellationToken.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/CancellationToken.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/TwitchIrc.cpp
|
||||||
|
${CMAKE_CURRENT_LIST_DIR}/src/IgnoreController.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp
|
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp
|
||||||
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.hpp
|
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.hpp
|
||||||
# Add your new file above this line!
|
# Add your new file above this line!
|
||||||
|
|
186
tests/src/IgnoreController.cpp
Normal file
186
tests/src/IgnoreController.cpp
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
#include "controllers/ignores/IgnoreController.hpp"
|
||||||
|
|
||||||
|
#include "controllers/accounts/AccountController.hpp"
|
||||||
|
#include "mocks/BaseApplication.hpp"
|
||||||
|
#include "mocks/Emotes.hpp"
|
||||||
|
#include "providers/twitch/TwitchIrc.hpp"
|
||||||
|
#include "Test.hpp"
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class MockApplication : public mock::BaseApplication
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MockApplication() = default;
|
||||||
|
|
||||||
|
IEmotes *getEmotes() override
|
||||||
|
{
|
||||||
|
return &this->emotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountController *getAccounts() override
|
||||||
|
{
|
||||||
|
return &this->accounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
mock::Emotes emotes;
|
||||||
|
AccountController accounts;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class TestIgnoreController : public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
void SetUp() override
|
||||||
|
{
|
||||||
|
this->mockApplication = std::make_unique<MockApplication>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override
|
||||||
|
{
|
||||||
|
this->mockApplication.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<MockApplication> mockApplication;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(TestIgnoreController, processIgnorePhrases)
|
||||||
|
{
|
||||||
|
struct TestCase {
|
||||||
|
std::vector<IgnorePhrase> phrases;
|
||||||
|
QString input;
|
||||||
|
std::vector<TwitchEmoteOccurrence> twitchEmotes;
|
||||||
|
QString expectedMessage;
|
||||||
|
std::vector<TwitchEmoteOccurrence> expectedTwitchEmotes;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto *twitchEmotes = this->mockApplication->getEmotes()->getTwitchEmotes();
|
||||||
|
|
||||||
|
auto emoteAt = [&](int at, const QString &name) {
|
||||||
|
return TwitchEmoteOccurrence{
|
||||||
|
.start = at,
|
||||||
|
.end = static_cast<int>(at + name.size() - 1),
|
||||||
|
.ptr =
|
||||||
|
twitchEmotes->getOrCreateEmote(EmoteId{name}, EmoteName{name}),
|
||||||
|
.name = EmoteName{name},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto regularReplace = [](auto pattern, auto replace,
|
||||||
|
bool caseSensitive = true) {
|
||||||
|
return IgnorePhrase(pattern, false, false, replace, caseSensitive);
|
||||||
|
};
|
||||||
|
auto regexReplace = [](auto pattern, auto regex,
|
||||||
|
bool caseSensitive = true) {
|
||||||
|
return IgnorePhrase(pattern, true, false, regex, caseSensitive);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TestCase> testCases{
|
||||||
|
{
|
||||||
|
{regularReplace("foo1", "baz1")},
|
||||||
|
"foo1 Kappa",
|
||||||
|
{emoteAt(4, "Kappa")},
|
||||||
|
"baz1 Kappa",
|
||||||
|
{emoteAt(4, "Kappa")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{regularReplace("foo1", "baz1", false)},
|
||||||
|
"FoO1 Kappa",
|
||||||
|
{emoteAt(4, "Kappa")},
|
||||||
|
"baz1 Kappa",
|
||||||
|
{emoteAt(4, "Kappa")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{regexReplace("f(o+)1", "baz1[\\1]")},
|
||||||
|
"foo1 Kappa",
|
||||||
|
{emoteAt(4, "Kappa")},
|
||||||
|
"baz1[oo] Kappa",
|
||||||
|
{emoteAt(8, "Kappa")},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
{regexReplace("f(o+)1", R"(baz1[\0][\1][\2])")},
|
||||||
|
"foo1 Kappa",
|
||||||
|
{emoteAt(4, "Kappa")},
|
||||||
|
"baz1[\\0][oo][\\2] Kappa",
|
||||||
|
{emoteAt(16, "Kappa")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{regexReplace("f(o+)(\\d+)", "baz1[\\1+\\2]")},
|
||||||
|
"foo123 Kappa",
|
||||||
|
{emoteAt(6, "Kappa")},
|
||||||
|
"baz1[oo+123] Kappa",
|
||||||
|
{emoteAt(12, "Kappa")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{regexReplace("(?<=foo)(\\d+)", "[\\1]")},
|
||||||
|
"foo123 Kappa",
|
||||||
|
{emoteAt(6, "Kappa")},
|
||||||
|
"foo[123] Kappa",
|
||||||
|
{emoteAt(8, "Kappa")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{regexReplace("a(?=a| )", "b")},
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
"aaaa"
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa "
|
||||||
|
"Kappa",
|
||||||
|
{emoteAt(127, "Kappa")},
|
||||||
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||||
|
"bbbb"
|
||||||
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb "
|
||||||
|
"Kappa",
|
||||||
|
{emoteAt(127, "Kappa")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{regexReplace("abc", "def", false)},
|
||||||
|
"AbC Kappa",
|
||||||
|
{emoteAt(3, "Kappa")},
|
||||||
|
"def Kappa",
|
||||||
|
{emoteAt(3, "Kappa")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
regexReplace("abc", "def", false),
|
||||||
|
regularReplace("def", "ghi"),
|
||||||
|
},
|
||||||
|
"AbC Kappa",
|
||||||
|
{emoteAt(3, "Kappa")},
|
||||||
|
"ghi Kappa",
|
||||||
|
{emoteAt(3, "Kappa")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{
|
||||||
|
regexReplace("a(?=a| )", "b"),
|
||||||
|
regexReplace("b(?=b| )", "c"),
|
||||||
|
},
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
"aaaa"
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa "
|
||||||
|
"Kappa",
|
||||||
|
{emoteAt(127, "Kappa")},
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||||
|
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc "
|
||||||
|
"Kappa",
|
||||||
|
{emoteAt(127, "Kappa")},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &test : testCases)
|
||||||
|
{
|
||||||
|
auto message = test.input;
|
||||||
|
auto emotes = test.twitchEmotes;
|
||||||
|
processIgnorePhrases(test.phrases, message, emotes);
|
||||||
|
|
||||||
|
EXPECT_EQ(message, test.expectedMessage)
|
||||||
|
<< "Message not equal for input '" << test.input
|
||||||
|
<< "' - expected: '" << test.expectedMessage << "' got: '"
|
||||||
|
<< message << "'";
|
||||||
|
EXPECT_EQ(emotes, test.expectedTwitchEmotes)
|
||||||
|
<< "Twitch emotes not equal for input '" << test.input
|
||||||
|
<< "' and output '" << message << "'";
|
||||||
|
}
|
||||||
|
}
|
|
@ -447,454 +447,6 @@ QT_WARNING_POP
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST(MessageBuilder, CommaSeparatedListTagParsing)
|
|
||||||
{
|
|
||||||
struct TestCase {
|
|
||||||
QString input;
|
|
||||||
std::pair<QString, QString> expectedOutput;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<TestCase> testCases{
|
|
||||||
{
|
|
||||||
"broadcaster/1",
|
|
||||||
{"broadcaster", "1"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"predictions/foo/bar/baz",
|
|
||||||
{"predictions", "foo/bar/baz"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"test/",
|
|
||||||
{"test", ""},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"/",
|
|
||||||
{"", ""},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"/value",
|
|
||||||
{"", "value"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"",
|
|
||||||
{"", ""},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto &test : testCases)
|
|
||||||
{
|
|
||||||
auto output = slashKeyValue(test.input);
|
|
||||||
|
|
||||||
EXPECT_EQ(output, test.expectedOutput)
|
|
||||||
<< "Input " << test.input << " failed";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestMessageBuilder : public ::testing::Test
|
|
||||||
{
|
|
||||||
protected:
|
|
||||||
void SetUp() override
|
|
||||||
{
|
|
||||||
this->mockApplication = std::make_unique<MockApplication>();
|
|
||||||
}
|
|
||||||
|
|
||||||
void TearDown() override
|
|
||||||
{
|
|
||||||
this->mockApplication.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<MockApplication> mockApplication;
|
|
||||||
};
|
|
||||||
|
|
||||||
TEST(MessageBuilder, BadgeInfoParsing)
|
|
||||||
{
|
|
||||||
struct TestCase {
|
|
||||||
QByteArray input;
|
|
||||||
std::unordered_map<QString, QString> expectedBadgeInfo;
|
|
||||||
std::vector<Badge> expectedBadges;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<TestCase> testCases{
|
|
||||||
{
|
|
||||||
R"(@badge-info=predictions/<<<<<<\sHEAD[15A⸝asdf/test;badges=predictions/pink-2;client-nonce=9dbb88e516edf4efb055c011f91ea0cf;color=#FF4500;display-name=もっと頑張って;emotes=;first-msg=0;flags=;id=feb00b12-4ec5-4f77-9160-667de463dab1;mod=0;room-id=99631238;subscriber=0;tmi-sent-ts=1653494874297;turbo=0;user-id=648946956;user-type= :zniksbot!zniksbot@zniksbot.tmi.twitch.tv PRIVMSG #zneix :-tags")",
|
|
||||||
{
|
|
||||||
{"predictions", R"(<<<<<<\sHEAD[15A⸝asdf/test)"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Badge{"predictions", "pink-2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
R"(@badge-info=predictions/<<<<<<\sHEAD[15A⸝asdf/test,founder/17;badges=predictions/pink-2,vip/1,founder/0,bits/1;client-nonce=9b836e232170a9df213aefdcb458b67e;color=#696969;display-name=NotKarar;emotes=;first-msg=0;flags=;id=e00881bd-5f21-4993-8bbd-1736cd13d42e;mod=0;room-id=99631238;subscriber=1;tmi-sent-ts=1653494879409;turbo=0;user-id=89954186;user-type= :notkarar!notkarar@notkarar.tmi.twitch.tv PRIVMSG #zneix :-tags)",
|
|
||||||
{
|
|
||||||
{"predictions", R"(<<<<<<\sHEAD[15A⸝asdf/test)"},
|
|
||||||
{"founder", "17"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Badge{"predictions", "pink-2"},
|
|
||||||
Badge{"vip", "1"},
|
|
||||||
Badge{"founder", "0"},
|
|
||||||
Badge{"bits", "1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
R"(@badge-info=predictions/foo/bar/baz;badges=predictions/blue-1,moderator/1,glhf-pledge/1;client-nonce=f73f16228e6e32f8e92b47ab8283b7e1;color=#1E90FF;display-name=zneixbot;emotes=30259:6-12;first-msg=0;flags=;id=9682a5f1-a0b0-45e2-be9f-8074b58c5f8f;mod=1;room-id=99631238;subscriber=0;tmi-sent-ts=1653573594035;turbo=0;user-id=463521670;user-type=mod :zneixbot!zneixbot@zneixbot.tmi.twitch.tv PRIVMSG #zneix :-tags HeyGuys)",
|
|
||||||
{
|
|
||||||
{"predictions", "foo/bar/baz"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Badge{"predictions", "blue-1"},
|
|
||||||
Badge{"moderator", "1"},
|
|
||||||
Badge{"glhf-pledge", "1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
R"(@badge-info=subscriber/22;badges=broadcaster/1,subscriber/18,glhf-pledge/1;color=#F97304;display-name=zneix;emotes=;first-msg=0;flags=;id=1d99f67f-a566-4416-a4e2-e85d7fce9223;mod=0;room-id=99631238;subscriber=1;tmi-sent-ts=1653612232758;turbo=0;user-id=99631238;user-type= :zneix!zneix@zneix.tmi.twitch.tv PRIVMSG #zneix :-tags)",
|
|
||||||
{
|
|
||||||
{"subscriber", "22"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Badge{"broadcaster", "1"},
|
|
||||||
Badge{"subscriber", "18"},
|
|
||||||
Badge{"glhf-pledge", "1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto &test : testCases)
|
|
||||||
{
|
|
||||||
auto *privmsg =
|
|
||||||
Communi::IrcPrivateMessage::fromData(test.input, nullptr);
|
|
||||||
|
|
||||||
auto outputBadgeInfo =
|
|
||||||
MessageBuilder::parseBadgeInfoTag(privmsg->tags());
|
|
||||||
EXPECT_EQ(outputBadgeInfo, test.expectedBadgeInfo)
|
|
||||||
<< "Input for badgeInfo " << test.input << " failed";
|
|
||||||
|
|
||||||
auto outputBadges = MessageBuilder::parseBadgeTag(privmsg->tags());
|
|
||||||
EXPECT_EQ(outputBadges, test.expectedBadges)
|
|
||||||
<< "Input for badges " << test.input << " failed";
|
|
||||||
|
|
||||||
delete privmsg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(TestMessageBuilder, ParseTwitchEmotes)
|
|
||||||
{
|
|
||||||
struct TestCase {
|
|
||||||
QByteArray input;
|
|
||||||
std::vector<TwitchEmoteOccurrence> expectedTwitchEmotes;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto *twitchEmotes = this->mockApplication->getEmotes()->getTwitchEmotes();
|
|
||||||
|
|
||||||
std::vector<TestCase> testCases{
|
|
||||||
{
|
|
||||||
// action /me message
|
|
||||||
R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=90ef1e46-8baa-4bf2-9c54-272f39d6fa11;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662206235860;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :ACTION Kappa)",
|
|
||||||
{
|
|
||||||
{{
|
|
||||||
0, // start
|
|
||||||
4, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(EmoteId{"25"},
|
|
||||||
EmoteName{"Kappa"}), // ptr
|
|
||||||
EmoteName{"Kappa"}, // name
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
R"(@badge-info=subscriber/17;badges=subscriber/12,no_audio/1;color=#EBA2C0;display-name=jammehcow;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662201093248;turbo=0;user-id=82674227;user-type= :jammehcow!jammehcow@jammehcow.tmi.twitch.tv PRIVMSG #pajlada :Kappa)",
|
|
||||||
{
|
|
||||||
{{
|
|
||||||
0, // start
|
|
||||||
4, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(EmoteId{"25"},
|
|
||||||
EmoteName{"Kappa"}), // ptr
|
|
||||||
EmoteName{"Kappa"}, // name
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=1902:0-4;first-msg=0;flags=;id=9b1c3cb9-7817-47ea-add1-f9d4a9b4f846;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201095690;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Keepo)",
|
|
||||||
{
|
|
||||||
{{
|
|
||||||
0, // start
|
|
||||||
4, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(EmoteId{"1902"},
|
|
||||||
EmoteName{"Keepo"}), // ptr
|
|
||||||
EmoteName{"Keepo"}, // name
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=25:0-4/1902:6-10/305954156:12-19;first-msg=0;flags=;id=7be87072-bf24-4fa3-b3df-0ea6fa5f1474;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201102276;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Kappa Keepo PogChamp)",
|
|
||||||
{
|
|
||||||
{
|
|
||||||
{
|
|
||||||
0, // start
|
|
||||||
4, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(
|
|
||||||
EmoteId{"25"}, EmoteName{"Kappa"}), // ptr
|
|
||||||
EmoteName{"Kappa"}, // name
|
|
||||||
},
|
|
||||||
{
|
|
||||||
6, // start
|
|
||||||
10, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(
|
|
||||||
EmoteId{"1902"}, EmoteName{"Keepo"}), // ptr
|
|
||||||
EmoteName{"Keepo"}, // name
|
|
||||||
},
|
|
||||||
{
|
|
||||||
12, // start
|
|
||||||
19, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(
|
|
||||||
EmoteId{"305954156"},
|
|
||||||
EmoteName{"PogChamp"}), // ptr
|
|
||||||
EmoteName{"PogChamp"}, // name
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4,6-10;first-msg=0;flags=;id=f7516287-e5d1-43ca-974e-fe0cff84400b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204375009;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa Kappa)",
|
|
||||||
{
|
|
||||||
{
|
|
||||||
{
|
|
||||||
0, // start
|
|
||||||
4, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(
|
|
||||||
EmoteId{"25"}, EmoteName{"Kappa"}), // ptr
|
|
||||||
EmoteName{"Kappa"}, // name
|
|
||||||
},
|
|
||||||
{
|
|
||||||
6, // start
|
|
||||||
10, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(
|
|
||||||
EmoteId{"25"}, EmoteName{"Kappa"}), // ptr
|
|
||||||
EmoteName{"Kappa"}, // name
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emotes=25:0-4,8-12;first-msg=0;flags=;id=44f85d39-b5fb-475d-8555-f4244f2f7e82;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204423418;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa 😂 Kappa)",
|
|
||||||
{
|
|
||||||
{
|
|
||||||
{
|
|
||||||
0, // start
|
|
||||||
4, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(
|
|
||||||
EmoteId{"25"}, EmoteName{"Kappa"}), // ptr
|
|
||||||
EmoteName{"Kappa"}, // name
|
|
||||||
},
|
|
||||||
{
|
|
||||||
9, // start - modified due to emoji
|
|
||||||
13, // end - modified due to emoji
|
|
||||||
twitchEmotes->getOrCreateEmote(
|
|
||||||
EmoteId{"25"}, EmoteName{"Kappa"}), // ptr
|
|
||||||
EmoteName{"Kappa"}, // name
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// start out of range
|
|
||||||
R"(@emotes=84608:9-10 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
|
||||||
{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// one character emote
|
|
||||||
R"(@emotes=84608:0-0 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
|
||||||
{
|
|
||||||
{
|
|
||||||
0, // start
|
|
||||||
0, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(EmoteId{"84608"},
|
|
||||||
EmoteName{"f"}), // ptr
|
|
||||||
EmoteName{"f"}, // name
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// two character emote
|
|
||||||
R"(@emotes=84609:0-1 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
|
||||||
{
|
|
||||||
{
|
|
||||||
0, // start
|
|
||||||
1, // end
|
|
||||||
twitchEmotes->getOrCreateEmote(EmoteId{"84609"},
|
|
||||||
EmoteName{"fo"}), // ptr
|
|
||||||
EmoteName{"fo"}, // name
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// end out of range
|
|
||||||
R"(@emotes=84608:0-15 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
|
||||||
{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// range bad (end character before start)
|
|
||||||
R"(@emotes=84608:15-2 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
|
||||||
{},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto &test : testCases)
|
|
||||||
{
|
|
||||||
auto *privmsg = dynamic_cast<Communi::IrcPrivateMessage *>(
|
|
||||||
Communi::IrcPrivateMessage::fromData(test.input, nullptr));
|
|
||||||
QString originalMessage = privmsg->content();
|
|
||||||
|
|
||||||
// TODO: Add tests with replies
|
|
||||||
auto actualTwitchEmotes = MessageBuilder::parseTwitchEmotes(
|
|
||||||
privmsg->tags(), originalMessage, 0);
|
|
||||||
|
|
||||||
EXPECT_EQ(actualTwitchEmotes, test.expectedTwitchEmotes)
|
|
||||||
<< "Input for twitch emotes " << test.input << " failed";
|
|
||||||
|
|
||||||
delete privmsg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(TestMessageBuilder, IgnoresReplace)
|
|
||||||
{
|
|
||||||
struct TestCase {
|
|
||||||
std::vector<IgnorePhrase> phrases;
|
|
||||||
QString input;
|
|
||||||
std::vector<TwitchEmoteOccurrence> twitchEmotes;
|
|
||||||
QString expectedMessage;
|
|
||||||
std::vector<TwitchEmoteOccurrence> expectedTwitchEmotes;
|
|
||||||
};
|
|
||||||
|
|
||||||
auto *twitchEmotes = this->mockApplication->getEmotes()->getTwitchEmotes();
|
|
||||||
|
|
||||||
auto emoteAt = [&](int at, const QString &name) {
|
|
||||||
return TwitchEmoteOccurrence{
|
|
||||||
.start = at,
|
|
||||||
.end = static_cast<int>(at + name.size() - 1),
|
|
||||||
.ptr =
|
|
||||||
twitchEmotes->getOrCreateEmote(EmoteId{name}, EmoteName{name}),
|
|
||||||
.name = EmoteName{name},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
auto regularReplace = [](auto pattern, auto replace,
|
|
||||||
bool caseSensitive = true) {
|
|
||||||
return IgnorePhrase(pattern, false, false, replace, caseSensitive);
|
|
||||||
};
|
|
||||||
auto regexReplace = [](auto pattern, auto regex,
|
|
||||||
bool caseSensitive = true) {
|
|
||||||
return IgnorePhrase(pattern, true, false, regex, caseSensitive);
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<TestCase> testCases{
|
|
||||||
{
|
|
||||||
{regularReplace("foo1", "baz1")},
|
|
||||||
"foo1 Kappa",
|
|
||||||
{emoteAt(4, "Kappa")},
|
|
||||||
"baz1 Kappa",
|
|
||||||
{emoteAt(4, "Kappa")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{regularReplace("foo1", "baz1", false)},
|
|
||||||
"FoO1 Kappa",
|
|
||||||
{emoteAt(4, "Kappa")},
|
|
||||||
"baz1 Kappa",
|
|
||||||
{emoteAt(4, "Kappa")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{regexReplace("f(o+)1", "baz1[\\1]")},
|
|
||||||
"foo1 Kappa",
|
|
||||||
{emoteAt(4, "Kappa")},
|
|
||||||
"baz1[oo] Kappa",
|
|
||||||
{emoteAt(8, "Kappa")},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
{regexReplace("f(o+)1", R"(baz1[\0][\1][\2])")},
|
|
||||||
"foo1 Kappa",
|
|
||||||
{emoteAt(4, "Kappa")},
|
|
||||||
"baz1[\\0][oo][\\2] Kappa",
|
|
||||||
{emoteAt(16, "Kappa")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{regexReplace("f(o+)(\\d+)", "baz1[\\1+\\2]")},
|
|
||||||
"foo123 Kappa",
|
|
||||||
{emoteAt(6, "Kappa")},
|
|
||||||
"baz1[oo+123] Kappa",
|
|
||||||
{emoteAt(12, "Kappa")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{regexReplace("(?<=foo)(\\d+)", "[\\1]")},
|
|
||||||
"foo123 Kappa",
|
|
||||||
{emoteAt(6, "Kappa")},
|
|
||||||
"foo[123] Kappa",
|
|
||||||
{emoteAt(8, "Kappa")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{regexReplace("a(?=a| )", "b")},
|
|
||||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
||||||
"aaaa"
|
|
||||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa "
|
|
||||||
"Kappa",
|
|
||||||
{emoteAt(127, "Kappa")},
|
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
|
||||||
"bbbb"
|
|
||||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb "
|
|
||||||
"Kappa",
|
|
||||||
{emoteAt(127, "Kappa")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{regexReplace("abc", "def", false)},
|
|
||||||
"AbC Kappa",
|
|
||||||
{emoteAt(3, "Kappa")},
|
|
||||||
"def Kappa",
|
|
||||||
{emoteAt(3, "Kappa")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{
|
|
||||||
regexReplace("abc", "def", false),
|
|
||||||
regularReplace("def", "ghi"),
|
|
||||||
},
|
|
||||||
"AbC Kappa",
|
|
||||||
{emoteAt(3, "Kappa")},
|
|
||||||
"ghi Kappa",
|
|
||||||
{emoteAt(3, "Kappa")},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
{
|
|
||||||
regexReplace("a(?=a| )", "b"),
|
|
||||||
regexReplace("b(?=b| )", "c"),
|
|
||||||
},
|
|
||||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
||||||
"aaaa"
|
|
||||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa "
|
|
||||||
"Kappa",
|
|
||||||
{emoteAt(127, "Kappa")},
|
|
||||||
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
|
||||||
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc "
|
|
||||||
"Kappa",
|
|
||||||
{emoteAt(127, "Kappa")},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto &test : testCases)
|
|
||||||
{
|
|
||||||
auto message = test.input;
|
|
||||||
auto emotes = test.twitchEmotes;
|
|
||||||
MessageBuilder::processIgnorePhrases(test.phrases, message, emotes);
|
|
||||||
|
|
||||||
EXPECT_EQ(message, test.expectedMessage)
|
|
||||||
<< "Message not equal for input '" << test.input
|
|
||||||
<< "' - expected: '" << test.expectedMessage << "' got: '"
|
|
||||||
<< message << "'";
|
|
||||||
EXPECT_EQ(emotes, test.expectedTwitchEmotes)
|
|
||||||
<< "Twitch emotes not equal for input '" << test.input
|
|
||||||
<< "' and output '" << message << "'";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestMessageBuilderP : public ::testing::TestWithParam<QString>
|
class TestMessageBuilderP : public ::testing::TestWithParam<QString>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|
336
tests/src/TwitchIrc.cpp
Normal file
336
tests/src/TwitchIrc.cpp
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
#include "providers/twitch/TwitchIrc.hpp"
|
||||||
|
|
||||||
|
#include "mocks/BaseApplication.hpp"
|
||||||
|
#include "mocks/Emotes.hpp"
|
||||||
|
#include "providers/twitch/TwitchBadge.hpp"
|
||||||
|
#include "Test.hpp"
|
||||||
|
#include "util/IrcHelpers.hpp"
|
||||||
|
|
||||||
|
using namespace chatterino;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class MockApplication : public mock::BaseApplication
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MockApplication() = default;
|
||||||
|
|
||||||
|
IEmotes *getEmotes() override
|
||||||
|
{
|
||||||
|
return &this->emotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
mock::Emotes emotes;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class TestTwitchIrc : public ::testing::Test
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
void SetUp() override
|
||||||
|
{
|
||||||
|
this->mockApplication = std::make_unique<MockApplication>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override
|
||||||
|
{
|
||||||
|
this->mockApplication.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<MockApplication> mockApplication;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST(TwitchIrc, CommaSeparatedListTagParsing)
|
||||||
|
{
|
||||||
|
struct TestCase {
|
||||||
|
QString input;
|
||||||
|
std::pair<QString, QString> expectedOutput;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TestCase> testCases{
|
||||||
|
{
|
||||||
|
"broadcaster/1",
|
||||||
|
{"broadcaster", "1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"predictions/foo/bar/baz",
|
||||||
|
{"predictions", "foo/bar/baz"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test/",
|
||||||
|
{"test", ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/",
|
||||||
|
{"", ""},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/value",
|
||||||
|
{"", "value"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"",
|
||||||
|
{"", ""},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &test : testCases)
|
||||||
|
{
|
||||||
|
auto output = slashKeyValue(test.input);
|
||||||
|
|
||||||
|
EXPECT_EQ(output, test.expectedOutput)
|
||||||
|
<< "Input " << test.input << " failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(TwitchIrc, BadgeInfoParsing)
|
||||||
|
{
|
||||||
|
struct TestCase {
|
||||||
|
QByteArray input;
|
||||||
|
std::unordered_map<QString, QString> expectedBadgeInfo;
|
||||||
|
std::vector<Badge> expectedBadges;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<TestCase> testCases{
|
||||||
|
{
|
||||||
|
R"(@badge-info=predictions/<<<<<<\sHEAD[15A⸝asdf/test;badges=predictions/pink-2;client-nonce=9dbb88e516edf4efb055c011f91ea0cf;color=#FF4500;display-name=もっと頑張って;emotes=;first-msg=0;flags=;id=feb00b12-4ec5-4f77-9160-667de463dab1;mod=0;room-id=99631238;subscriber=0;tmi-sent-ts=1653494874297;turbo=0;user-id=648946956;user-type= :zniksbot!zniksbot@zniksbot.tmi.twitch.tv PRIVMSG #zneix :-tags")",
|
||||||
|
{
|
||||||
|
{"predictions", R"(<<<<<<\sHEAD[15A⸝asdf/test)"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Badge{"predictions", "pink-2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
R"(@badge-info=predictions/<<<<<<\sHEAD[15A⸝asdf/test,founder/17;badges=predictions/pink-2,vip/1,founder/0,bits/1;client-nonce=9b836e232170a9df213aefdcb458b67e;color=#696969;display-name=NotKarar;emotes=;first-msg=0;flags=;id=e00881bd-5f21-4993-8bbd-1736cd13d42e;mod=0;room-id=99631238;subscriber=1;tmi-sent-ts=1653494879409;turbo=0;user-id=89954186;user-type= :notkarar!notkarar@notkarar.tmi.twitch.tv PRIVMSG #zneix :-tags)",
|
||||||
|
{
|
||||||
|
{"predictions", R"(<<<<<<\sHEAD[15A⸝asdf/test)"},
|
||||||
|
{"founder", "17"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Badge{"predictions", "pink-2"},
|
||||||
|
Badge{"vip", "1"},
|
||||||
|
Badge{"founder", "0"},
|
||||||
|
Badge{"bits", "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
R"(@badge-info=predictions/foo/bar/baz;badges=predictions/blue-1,moderator/1,glhf-pledge/1;client-nonce=f73f16228e6e32f8e92b47ab8283b7e1;color=#1E90FF;display-name=zneixbot;emotes=30259:6-12;first-msg=0;flags=;id=9682a5f1-a0b0-45e2-be9f-8074b58c5f8f;mod=1;room-id=99631238;subscriber=0;tmi-sent-ts=1653573594035;turbo=0;user-id=463521670;user-type=mod :zneixbot!zneixbot@zneixbot.tmi.twitch.tv PRIVMSG #zneix :-tags HeyGuys)",
|
||||||
|
{
|
||||||
|
{"predictions", "foo/bar/baz"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Badge{"predictions", "blue-1"},
|
||||||
|
Badge{"moderator", "1"},
|
||||||
|
Badge{"glhf-pledge", "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
R"(@badge-info=subscriber/22;badges=broadcaster/1,subscriber/18,glhf-pledge/1;color=#F97304;display-name=zneix;emotes=;first-msg=0;flags=;id=1d99f67f-a566-4416-a4e2-e85d7fce9223;mod=0;room-id=99631238;subscriber=1;tmi-sent-ts=1653612232758;turbo=0;user-id=99631238;user-type= :zneix!zneix@zneix.tmi.twitch.tv PRIVMSG #zneix :-tags)",
|
||||||
|
{
|
||||||
|
{"subscriber", "22"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Badge{"broadcaster", "1"},
|
||||||
|
Badge{"subscriber", "18"},
|
||||||
|
Badge{"glhf-pledge", "1"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &test : testCases)
|
||||||
|
{
|
||||||
|
auto *privmsg =
|
||||||
|
Communi::IrcPrivateMessage::fromData(test.input, nullptr);
|
||||||
|
|
||||||
|
auto outputBadgeInfo = parseBadgeInfoTag(privmsg->tags());
|
||||||
|
EXPECT_EQ(outputBadgeInfo, test.expectedBadgeInfo)
|
||||||
|
<< "Input for badgeInfo " << test.input << " failed";
|
||||||
|
|
||||||
|
auto outputBadges = parseBadgeTag(privmsg->tags());
|
||||||
|
EXPECT_EQ(outputBadges, test.expectedBadges)
|
||||||
|
<< "Input for badges " << test.input << " failed";
|
||||||
|
|
||||||
|
delete privmsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(TestTwitchIrc, ParseTwitchEmotes)
|
||||||
|
{
|
||||||
|
struct TestCase {
|
||||||
|
QByteArray input;
|
||||||
|
std::vector<TwitchEmoteOccurrence> expectedTwitchEmotes;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto *twitchEmotes = this->mockApplication->getEmotes()->getTwitchEmotes();
|
||||||
|
|
||||||
|
std::vector<TestCase> testCases{
|
||||||
|
{
|
||||||
|
// action /me message
|
||||||
|
R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=90ef1e46-8baa-4bf2-9c54-272f39d6fa11;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662206235860;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :ACTION Kappa)",
|
||||||
|
{
|
||||||
|
{{
|
||||||
|
0, // start
|
||||||
|
4, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(EmoteId{"25"},
|
||||||
|
EmoteName{"Kappa"}), // ptr
|
||||||
|
EmoteName{"Kappa"}, // name
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
R"(@badge-info=subscriber/17;badges=subscriber/12,no_audio/1;color=#EBA2C0;display-name=jammehcow;emote-only=1;emotes=25:0-4;first-msg=0;flags=;id=9c2dd916-5a6d-4c1f-9fe7-a081b62a9c6b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662201093248;turbo=0;user-id=82674227;user-type= :jammehcow!jammehcow@jammehcow.tmi.twitch.tv PRIVMSG #pajlada :Kappa)",
|
||||||
|
{
|
||||||
|
{{
|
||||||
|
0, // start
|
||||||
|
4, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(EmoteId{"25"},
|
||||||
|
EmoteName{"Kappa"}), // ptr
|
||||||
|
EmoteName{"Kappa"}, // name
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=1902:0-4;first-msg=0;flags=;id=9b1c3cb9-7817-47ea-add1-f9d4a9b4f846;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201095690;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Keepo)",
|
||||||
|
{
|
||||||
|
{{
|
||||||
|
0, // start
|
||||||
|
4, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(EmoteId{"1902"},
|
||||||
|
EmoteName{"Keepo"}), // ptr
|
||||||
|
EmoteName{"Keepo"}, // name
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
R"(@badge-info=;badges=no_audio/1;color=#DAA520;display-name=Mm2PL;emote-only=1;emotes=25:0-4/1902:6-10/305954156:12-19;first-msg=0;flags=;id=7be87072-bf24-4fa3-b3df-0ea6fa5f1474;mod=0;returning-chatter=0;room-id=11148817;subscriber=0;tmi-sent-ts=1662201102276;turbo=0;user-id=117691339;user-type= :mm2pl!mm2pl@mm2pl.tmi.twitch.tv PRIVMSG #pajlada :Kappa Keepo PogChamp)",
|
||||||
|
{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
0, // start
|
||||||
|
4, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(
|
||||||
|
EmoteId{"25"}, EmoteName{"Kappa"}), // ptr
|
||||||
|
EmoteName{"Kappa"}, // name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
6, // start
|
||||||
|
10, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(
|
||||||
|
EmoteId{"1902"}, EmoteName{"Keepo"}), // ptr
|
||||||
|
EmoteName{"Keepo"}, // name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
12, // start
|
||||||
|
19, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(
|
||||||
|
EmoteId{"305954156"},
|
||||||
|
EmoteName{"PogChamp"}), // ptr
|
||||||
|
EmoteName{"PogChamp"}, // name
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emote-only=1;emotes=25:0-4,6-10;first-msg=0;flags=;id=f7516287-e5d1-43ca-974e-fe0cff84400b;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204375009;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa Kappa)",
|
||||||
|
{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
0, // start
|
||||||
|
4, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(
|
||||||
|
EmoteId{"25"}, EmoteName{"Kappa"}), // ptr
|
||||||
|
EmoteName{"Kappa"}, // name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
6, // start
|
||||||
|
10, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(
|
||||||
|
EmoteId{"25"}, EmoteName{"Kappa"}), // ptr
|
||||||
|
EmoteName{"Kappa"}, // name
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
R"(@badge-info=subscriber/80;badges=broadcaster/1,subscriber/3072,partner/1;color=#CC44FF;display-name=pajlada;emotes=25:0-4,8-12;first-msg=0;flags=;id=44f85d39-b5fb-475d-8555-f4244f2f7e82;mod=0;returning-chatter=0;room-id=11148817;subscriber=1;tmi-sent-ts=1662204423418;turbo=0;user-id=11148817;user-type= :pajlada!pajlada@pajlada.tmi.twitch.tv PRIVMSG #pajlada :Kappa 😂 Kappa)",
|
||||||
|
{
|
||||||
|
{
|
||||||
|
{
|
||||||
|
0, // start
|
||||||
|
4, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(
|
||||||
|
EmoteId{"25"}, EmoteName{"Kappa"}), // ptr
|
||||||
|
EmoteName{"Kappa"}, // name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
9, // start - modified due to emoji
|
||||||
|
13, // end - modified due to emoji
|
||||||
|
twitchEmotes->getOrCreateEmote(
|
||||||
|
EmoteId{"25"}, EmoteName{"Kappa"}), // ptr
|
||||||
|
EmoteName{"Kappa"}, // name
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// start out of range
|
||||||
|
R"(@emotes=84608:9-10 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// one character emote
|
||||||
|
R"(@emotes=84608:0-0 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
||||||
|
{
|
||||||
|
{
|
||||||
|
0, // start
|
||||||
|
0, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(EmoteId{"84608"},
|
||||||
|
EmoteName{"f"}), // ptr
|
||||||
|
EmoteName{"f"}, // name
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// two character emote
|
||||||
|
R"(@emotes=84609:0-1 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
||||||
|
{
|
||||||
|
{
|
||||||
|
0, // start
|
||||||
|
1, // end
|
||||||
|
twitchEmotes->getOrCreateEmote(EmoteId{"84609"},
|
||||||
|
EmoteName{"fo"}), // ptr
|
||||||
|
EmoteName{"fo"}, // name
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// end out of range
|
||||||
|
R"(@emotes=84608:0-15 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// range bad (end character before start)
|
||||||
|
R"(@emotes=84608:15-2 :test!test@test.tmi.twitch.tv PRIVMSG #pajlada :foo bar)",
|
||||||
|
{},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &test : testCases)
|
||||||
|
{
|
||||||
|
auto *privmsg = dynamic_cast<Communi::IrcPrivateMessage *>(
|
||||||
|
Communi::IrcPrivateMessage::fromData(test.input, nullptr));
|
||||||
|
ASSERT_NE(privmsg, nullptr);
|
||||||
|
QString originalMessage = privmsg->content();
|
||||||
|
|
||||||
|
// TODO: Add tests with replies
|
||||||
|
auto actualTwitchEmotes =
|
||||||
|
parseTwitchEmotes(privmsg->tags(), originalMessage, 0);
|
||||||
|
|
||||||
|
EXPECT_EQ(actualTwitchEmotes, test.expectedTwitchEmotes)
|
||||||
|
<< "Input for twitch emotes " << test.input << " failed";
|
||||||
|
|
||||||
|
delete privmsg;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue