Merge branch 'blacklist' into blacklistnew

This commit is contained in:
hemirt 2018-09-23 20:05:14 +02:00
commit ea07bbef0b
8 changed files with 414 additions and 211 deletions

@ -1 +1 @@
Subproject commit bcbd29b793a2895ddf1772dca0105f51a0380798 Subproject commit 7f0db95f245fb726e756ecde15a800c0928b054b

@ -1 +1 @@
Subproject commit a1b61606c144c34a0f0f9173768cb85dd58e7f29 Subproject commit e03c868ec922027a0e672b64388808beb1297816

View file

@ -8,7 +8,7 @@ namespace chatterino {
// commandmodel // commandmodel
IgnoreModel::IgnoreModel(QObject *parent) IgnoreModel::IgnoreModel(QObject *parent)
: SignalVectorModel<IgnorePhrase>(2, parent) : SignalVectorModel<IgnorePhrase>(5, parent)
{ {
} }
@ -18,8 +18,10 @@ IgnorePhrase IgnoreModel::getItemFromRow(std::vector<QStandardItem *> &row,
{ {
// key, regex // key, regex
return IgnorePhrase{row[0]->data(Qt::DisplayRole).toString(), return IgnorePhrase{
row[1]->data(Qt::CheckStateRole).toBool()}; row[0]->data(Qt::DisplayRole).toString(), row[1]->data(Qt::CheckStateRole).toBool(),
row[3]->data(Qt::CheckStateRole).toBool(), row[4]->data(Qt::DisplayRole).toString(),
row[2]->data(Qt::CheckStateRole).toBool()};
} }
// turns a row in the model into a vector item // turns a row in the model into a vector item
@ -28,6 +30,9 @@ void IgnoreModel::getRowFromItem(const IgnorePhrase &item,
{ {
setStringItem(row[0], item.getPattern()); setStringItem(row[0], item.getPattern());
setBoolItem(row[1], item.isRegex()); setBoolItem(row[1], item.isRegex());
setBoolItem(row[2], item.isCaseSensitive());
setBoolItem(row[3], item.isBlock());
setStringItem(row[4], item.getReplace());
} }
} // namespace chatterino } // namespace chatterino

View file

@ -1,5 +1,9 @@
#pragma once #pragma once
#include "Application.hpp"
#include "controllers/accounts/AccountController.hpp"
#include "singletons/Settings.hpp"
#include "util/RapidJsonSerializeQString.hpp" #include "util/RapidJsonSerializeQString.hpp"
#include "util/RapidjsonHelpers.hpp" #include "util/RapidjsonHelpers.hpp"
@ -16,80 +20,154 @@ class IgnorePhrase
public: public:
bool operator==(const IgnorePhrase &other) const bool operator==(const IgnorePhrase &other) const
{ {
return std::tie(this->pattern_, this->isRegex_) == return std::tie(this->pattern_, this->isRegex_, this->isBlock_, this->replace_,
std::tie(other.pattern_, other.isRegex_); this->isCaseSensitive_) == std::tie(other.pattern_, other.isRegex_,
other.isBlock_, other.replace_,
other.isCaseSensitive_);
} }
IgnorePhrase(const QString &pattern, bool isRegex) IgnorePhrase(const QString &pattern, bool isRegex, bool isBlock, const QString &replace,
bool isCaseSensitive)
: pattern_(pattern) : pattern_(pattern)
, isRegex_(isRegex) , isRegex_(isRegex)
, regex_(isRegex_ ? pattern , regex_(pattern)
: "\\b" + QRegularExpression::escape(pattern) + "\\b", , isBlock_(isBlock)
QRegularExpression::CaseInsensitiveOption | , replace_(replace)
QRegularExpression::UseUnicodePropertiesOption) , isCaseSensitive_(isCaseSensitive)
{ {
if (this->isCaseSensitive_) {
regex_.setPatternOptions(QRegularExpression::UseUnicodePropertiesOption);
} else {
regex_.setPatternOptions(QRegularExpression::CaseInsensitiveOption |
QRegularExpression::UseUnicodePropertiesOption);
}
} }
const QString &getPattern() const const QString &getPattern() const
{ {
return this->pattern_; return this->pattern_;
} }
bool isRegex() const bool isRegex() const
{ {
return this->isRegex_; return this->isRegex_;
} }
bool isValid() const bool isRegexValid() const
{ {
return !this->pattern_.isEmpty() && this->regex_.isValid(); return this->regex_.isValid();
} }
bool isMatch(const QString &subject) const bool isMatch(const QString &subject) const
{ {
return this->isValid() && this->regex_.match(subject).hasMatch(); return !this->pattern_.isEmpty() &&
(this->isRegex() ? (this->regex_.isValid() && this->regex_.match(subject).hasMatch())
: subject.contains(this->pattern_, this->caseSensitivity()));
}
const QRegularExpression &getRegex() const
{
return this->regex_;
}
bool isBlock() const
{
return this->isBlock_;
}
const QString &getReplace() const
{
return this->replace_;
}
bool isCaseSensitive() const
{
return this->isCaseSensitive_;
}
Qt::CaseSensitivity caseSensitivity() const
{
return this->isCaseSensitive_ ? Qt::CaseSensitive : Qt::CaseInsensitive;
}
const std::unordered_map<EmoteName, EmotePtr> &getEmotes() const
{
return this->emotes_;
}
bool containsEmote() const
{
if (!this->emotesChecked_) {
const auto &accvec = getApp()->accounts->twitch.accounts.getVector();
for (const auto &acc : accvec) {
const auto &accemotes = *acc->accessEmotes();
for (const auto &emote : accemotes.emotes) {
if (this->replace_.contains(emote.first.string, Qt::CaseSensitive)) {
this->emotes_.emplace(emote.first, emote.second);
}
}
}
this->emotesChecked_ = true;
}
return !this->emotes_.empty();
} }
private: private:
QString pattern_; QString pattern_;
bool isRegex_; bool isRegex_;
QRegularExpression regex_; QRegularExpression regex_;
bool isBlock_;
QString replace_;
bool isCaseSensitive_;
mutable std::unordered_map<EmoteName, EmotePtr> emotes_;
mutable bool emotesChecked_{false};
}; };
} // namespace chatterino } // namespace chatterino
namespace pajlada { namespace pajlada {
namespace Settings { namespace Settings {
template <> template <>
struct Serialize<chatterino::IgnorePhrase> { struct Serialize<chatterino::IgnorePhrase> {
static rapidjson::Value get(const chatterino::IgnorePhrase &value, static rapidjson::Value get(const chatterino::IgnorePhrase &value,
rapidjson::Document::AllocatorType &a) rapidjson::Document::AllocatorType &a)
{ {
rapidjson::Value ret(rapidjson::kObjectType); rapidjson::Value ret(rapidjson::kObjectType);
AddMember(ret, "pattern", value.getPattern(), a); AddMember(ret, "pattern", value.getPattern(), a);
AddMember(ret, "regex", value.isRegex(), a); AddMember(ret, "regex", value.isRegex(), a);
AddMember(ret, "isBlock", value.isBlock(), a);
AddMember(ret, "replaceWith", value.getReplace(), a);
AddMember(ret, "caseSensitive", value.isCaseSensitive(), a);
return ret; return ret;
}
};
template <>
struct Deserialize<chatterino::IgnorePhrase> {
static chatterino::IgnorePhrase get(const rapidjson::Value &value)
{
if (!value.IsObject()) {
return chatterino::IgnorePhrase(
QString(), false, false,
::chatterino::getSettings()->ignoredPhraseReplace.getValue(), true);
} }
};
template <> QString _pattern;
struct Deserialize<chatterino::IgnorePhrase> { bool _isRegex = false;
static chatterino::IgnorePhrase get(const rapidjson::Value &value) bool _isBlock = false;
{ QString _replace;
if (!value.IsObject()) { bool _caseSens = true;
return chatterino::IgnorePhrase(QString(), false);
}
QString _pattern; chatterino::rj::getSafe(value, "pattern", _pattern);
bool _isRegex = false; chatterino::rj::getSafe(value, "regex", _isRegex);
chatterino::rj::getSafe(value, "isBlock", _isBlock);
chatterino::rj::getSafe(value, "pattern", _pattern); chatterino::rj::getSafe(value, "replaceWith", _replace);
chatterino::rj::getSafe(value, "regex", _isRegex); chatterino::rj::getSafe(value, "caseSensitive", _caseSens);
return chatterino::IgnorePhrase(_pattern, _isRegex);
}
};
return chatterino::IgnorePhrase(_pattern, _isRegex, _isBlock, _replace, _caseSens);
}
};
} // namespace Settings } // namespace Settings
} // namespace pajlada } // namespace pajlada

View file

@ -21,13 +21,14 @@
#include <QApplication> #include <QApplication>
#include <QDebug> #include <QDebug>
#include <QMediaPlayer> #include <QMediaPlayer>
#include <QStringRef>
#include <boost/variant.hpp> #include <boost/variant.hpp>
namespace chatterino { namespace chatterino {
TwitchMessageBuilder::TwitchMessageBuilder( TwitchMessageBuilder::TwitchMessageBuilder(Channel *_channel,
Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage, const Communi::IrcPrivateMessage *_ircMessage,
const MessageParseArgs &_args) const MessageParseArgs &_args)
: channel(_channel) : channel(_channel)
, twitchChannel(dynamic_cast<TwitchChannel *>(_channel)) , twitchChannel(dynamic_cast<TwitchChannel *>(_channel))
, ircMessage(_ircMessage) , ircMessage(_ircMessage)
@ -39,9 +40,10 @@ TwitchMessageBuilder::TwitchMessageBuilder(
this->usernameColor_ = getApp()->themes->messages.textColors.system; this->usernameColor_ = getApp()->themes->messages.textColors.system;
} }
TwitchMessageBuilder::TwitchMessageBuilder( TwitchMessageBuilder::TwitchMessageBuilder(Channel *_channel,
Channel *_channel, const Communi::IrcMessage *_ircMessage, const Communi::IrcMessage *_ircMessage,
const MessageParseArgs &_args, QString content, bool isAction) const MessageParseArgs &_args, QString content,
bool isAction)
: channel(_channel) : channel(_channel)
, twitchChannel(dynamic_cast<TwitchChannel *>(_channel)) , twitchChannel(dynamic_cast<TwitchChannel *>(_channel))
, ircMessage(_ircMessage) , ircMessage(_ircMessage)
@ -59,22 +61,18 @@ bool TwitchMessageBuilder::isIgnored() const
// TODO(pajlada): Do we need to check if the phrase is valid first? // TODO(pajlada): Do we need to check if the phrase is valid first?
for (const auto &phrase : app->ignores->phrases.getVector()) { for (const auto &phrase : app->ignores->phrases.getVector()) {
if (phrase.isMatch(this->originalMessage_)) { if (phrase.isBlock() && phrase.isMatch(this->originalMessage_)) {
log("Blocking message because it contains ignored phrase {}", log("Blocking message because it contains ignored phrase {}", phrase.getPattern());
phrase.getPattern());
return true; return true;
} }
} }
if (getSettings()->enableTwitchIgnoredUsers && if (getSettings()->enableTwitchIgnoredUsers && this->tags.contains("user-id")) {
this->tags.contains("user-id")) {
auto sourceUserID = this->tags.value("user-id").toString(); auto sourceUserID = this->tags.value("user-id").toString();
for (const auto &user : for (const auto &user : app->accounts->twitch.getCurrent()->getIgnores()) {
app->accounts->twitch.getCurrent()->getIgnores()) {
if (sourceUserID == user.id) { if (sourceUserID == user.id) {
log("Blocking message because it's from blocked user {}", log("Blocking message because it's from blocked user {}", user.name);
user.name);
return true; return true;
} }
} }
@ -149,7 +147,7 @@ MessagePtr TwitchMessageBuilder::build()
} }
// twitch emotes // twitch emotes
std::vector<std::pair<int, EmotePtr>> twitchEmotes; std::vector<std::tuple<int, EmotePtr, EmoteName>> twitchEmotes;
iterator = this->tags.find("emotes"); iterator = this->tags.find("emotes");
if (iterator != this->tags.end()) { if (iterator != this->tags.end()) {
@ -158,11 +156,176 @@ MessagePtr TwitchMessageBuilder::build()
for (QString emote : emoteString) { for (QString emote : emoteString) {
this->appendTwitchEmote(emote, twitchEmotes); this->appendTwitchEmote(emote, twitchEmotes);
} }
std::sort(
twitchEmotes.begin(), twitchEmotes.end(),
[](const auto &a, const auto &b) { return a.first < b.first; });
} }
auto app = getApp();
const auto &phrases = app->ignores->phrases.getVector();
auto removeEmotesInRange =
[](int pos, int len,
std::vector<std::tuple<int, EmotePtr, EmoteName>> &twitchEmotes) mutable {
auto it = std::partition(
twitchEmotes.begin(), twitchEmotes.end(), [pos, len](const auto &item) {
return !((std::get<0>(item) >= pos) && std::get<0>(item) < (pos + len));
});
for (auto copy = it; copy != twitchEmotes.end(); ++copy) {
if (std::get<1>(*copy) == nullptr) {
log("remem nullptr {}", std::get<2>(*copy).string);
}
}
std::vector<std::tuple<int, EmotePtr, EmoteName>> v(it, twitchEmotes.end());
twitchEmotes.erase(it, twitchEmotes.end());
return v;
};
auto shiftIndicesAfter = [&twitchEmotes](int pos, int by) mutable {
for (auto &item : twitchEmotes) {
auto &index = std::get<0>(item);
if (index >= pos) {
index += by;
}
}
};
auto addReplEmotes = [&twitchEmotes](const IgnorePhrase &phrase, const QStringRef &midrepl,
int startIndex) mutable {
if (!phrase.containsEmote()) {
return;
}
QVector<QStringRef> words = midrepl.split(' ');
int pos = 0;
for (const auto &word : words) {
for (const auto &emote : phrase.getEmotes()) {
if (word == emote.first.string) {
if (emote.second == nullptr) {
log("emote null {}", emote.first.string);
}
twitchEmotes.push_back(std::tuple<int, EmotePtr, EmoteName>{
startIndex + pos, emote.second, emote.first});
}
}
pos += word.length() + 1;
}
};
for (const auto &phrase : phrases) {
if (phrase.isBlock()) {
continue;
}
if (phrase.isRegex()) {
const auto &regex = phrase.getRegex();
if (!regex.isValid()) {
continue;
}
QRegularExpressionMatch match;
int from = 0;
while ((from = this->originalMessage_.indexOf(regex, from, &match)) != -1) {
int len = match.capturedLength();
auto vret = removeEmotesInRange(from, len, twitchEmotes);
auto mid = this->originalMessage_.mid(from, len);
mid.replace(regex, phrase.getReplace());
int midsize = mid.size();
this->originalMessage_.replace(from, len, mid);
int pos1 = from;
while (pos1 > 0) {
if (this->originalMessage_[pos1 - 1] == ' ') {
break;
}
--pos1;
}
int pos2 = from + midsize;
while (pos2 < this->originalMessage_.length()) {
if (this->originalMessage_[pos2] == ' ') {
break;
}
++pos2;
}
shiftIndicesAfter(from + len, midsize - len);
auto midExtendedRef = this->originalMessage_.midRef(pos1, pos2 - pos1);
for (auto &tup : vret) {
if (std::get<1>(tup) == nullptr) {
log("v nullptr {}", std::get<2>(tup).string);
continue;
}
int index = 0;
QString emote = " " + std::get<2>(tup).string + " ";
while ((index = midExtendedRef.indexOf(emote, index)) != -1) {
std::get<0>(tup) = from + index + 1;
index += emote.size() - 1;
twitchEmotes.push_back(tup);
}
}
addReplEmotes(phrase, midExtendedRef, pos1);
from += midsize;
}
} else {
const auto &pattern = phrase.getPattern();
if (pattern.isEmpty()) {
continue;
}
int from = 0;
while ((from = this->originalMessage_.indexOf(pattern, from,
phrase.caseSensitivity())) != -1) {
int len = pattern.size();
auto vret = removeEmotesInRange(from, len, twitchEmotes);
auto replace = phrase.getReplace();
int replacesize = replace.size();
this->originalMessage_.replace(from, len, replace);
int pos1 = from;
while (pos1 > 0) {
if (this->originalMessage_[pos1 - 1] == ' ') {
break;
}
--pos1;
}
int pos2 = from + replacesize;
while (pos2 < this->originalMessage_.length()) {
if (this->originalMessage_[pos2] == ' ') {
break;
}
++pos2;
}
shiftIndicesAfter(from + len, replacesize - len);
auto midExtendedRef = this->originalMessage_.midRef(pos1, pos2 - pos1);
for (auto &tup : vret) {
if (std::get<1>(tup) == nullptr) {
log("v nullptr {}", std::get<2>(tup).string);
continue;
}
int index = 0;
QString emote = " " + std::get<2>(tup).string + " ";
while ((index = midExtendedRef.indexOf(emote, index)) != -1) {
std::get<0>(tup) = from + index + 1;
index += emote.size() - 1;
twitchEmotes.push_back(tup);
}
}
addReplEmotes(phrase, midExtendedRef, pos1);
from += replacesize;
}
}
}
std::sort(twitchEmotes.begin(), twitchEmotes.end(),
[](const auto &a, const auto &b) { return std::get<0>(a) < std::get<0>(b); });
twitchEmotes.erase(std::unique(twitchEmotes.begin(), twitchEmotes.end(),
[](const auto &first, const auto &second) {
return std::get<0>(first) == std::get<0>(second);
}),
twitchEmotes.end());
auto currentTwitchEmote = twitchEmotes.begin();
// words // words
QStringList splits = this->originalMessage_.split(' '); QStringList splits = this->originalMessage_.split(' ');
@ -175,19 +338,22 @@ MessagePtr TwitchMessageBuilder::build()
} }
void TwitchMessageBuilder::addWords( void TwitchMessageBuilder::addWords(
const QStringList &words, const QStringList &words, const std::vector<std::tuple<int, EmotePtr, EmoteName>> &twitchEmotes)
const std::vector<std::pair<int, EmotePtr>> &twitchEmotes)
{ {
auto i = int(); auto i = int();
auto currentTwitchEmote = twitchEmotes.begin(); auto currentTwitchEmote = twitchEmotes.begin();
for (const auto &word : words) { for (const auto &word : words) {
// check if it's a twitch emote twitch emote // check if it's a twitch emote twitch emote
if (currentTwitchEmote != twitchEmotes.end() && while (currentTwitchEmote != twitchEmotes.end() && std::get<0>(*currentTwitchEmote) < i) {
currentTwitchEmote->first == i) { ++currentTwitchEmote;
auto emoteImage = currentTwitchEmote->second; }
this->emplace<EmoteElement>(emoteImage, if (currentTwitchEmote != twitchEmotes.end() && std::get<0>(*currentTwitchEmote) == i) {
MessageElementFlag::TwitchEmote); auto emoteImage = std::get<1>(*currentTwitchEmote);
if (emoteImage == nullptr) {
log("emoteImage nullptr {}", std::get<2>(*currentTwitchEmote).string);
}
this->emplace<EmoteElement>(emoteImage, MessageElementFlag::TwitchEmote);
i += word.length() + 1; i += word.length() + 1;
currentTwitchEmote++; currentTwitchEmote++;
@ -197,8 +363,7 @@ void TwitchMessageBuilder::addWords(
// split words // split words
for (auto &variant : getApp()->emotes->emojis.parse(word)) { for (auto &variant : getApp()->emotes->emojis.parse(word)) {
boost::apply_visitor([&](auto &&arg) { this->addTextOrEmoji(arg); }, boost::apply_visitor([&](auto &&arg) { this->addTextOrEmoji(arg); }, variant);
variant);
} }
for (int j = 0; j < word.size(); j++) { for (int j = 0; j < word.size(); j++) {
@ -240,18 +405,16 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
// Actually just text // Actually just text
auto linkString = this->matchLink(string); auto linkString = this->matchLink(string);
auto link = Link(); auto link = Link();
auto textColor = this->action_ ? MessageColor(this->usernameColor_) auto textColor =
: MessageColor(MessageColor::Text); this->action_ ? MessageColor(this->usernameColor_) : MessageColor(MessageColor::Text);
if (linkString.isEmpty()) { if (linkString.isEmpty()) {
if (string.startsWith('@')) { if (string.startsWith('@')) {
this->emplace<TextElement>(string, MessageElementFlag::BoldUsername, this->emplace<TextElement>(string, MessageElementFlag::BoldUsername, textColor,
textColor, FontStyle::ChatMediumBold); FontStyle::ChatMediumBold);
this->emplace<TextElement>( this->emplace<TextElement>(string, MessageElementFlag::NonBoldUsername, textColor);
string, MessageElementFlag::NonBoldUsername, textColor);
} else { } else {
this->emplace<TextElement>(string, MessageElementFlag::Text, this->emplace<TextElement>(string, MessageElementFlag::Text, textColor);
textColor);
} }
} else { } else {
static QRegularExpression domainRegex( static QRegularExpression domainRegex(
@ -262,8 +425,7 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
auto match = domainRegex.match(string); auto match = domainRegex.match(string);
if (match.isValid()) { if (match.isValid()) {
lowercaseLinkString = string.mid(0, match.capturedStart(1)) + lowercaseLinkString = string.mid(0, match.capturedStart(1)) +
match.captured(1).toLower() + match.captured(1).toLower() + string.mid(match.capturedEnd(1));
string.mid(match.capturedEnd(1));
} else { } else {
lowercaseLinkString = string; lowercaseLinkString = string;
} }
@ -271,28 +433,24 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
textColor = MessageColor(MessageColor::Link); textColor = MessageColor(MessageColor::Link);
auto linkMELowercase = auto linkMELowercase =
this->emplace<TextElement>(lowercaseLinkString, this->emplace<TextElement>(lowercaseLinkString, MessageElementFlag::LowercaseLink,
MessageElementFlag::LowercaseLink,
textColor) textColor)
->setLink(link); ->setLink(link);
auto linkMEOriginal = auto linkMEOriginal =
this->emplace<TextElement>(string, MessageElementFlag::OriginalLink, this->emplace<TextElement>(string, MessageElementFlag::OriginalLink, textColor)
textColor)
->setLink(link); ->setLink(link);
LinkResolver::getLinkInfo( LinkResolver::getLinkInfo(linkString, [linkMELowercase, linkMEOriginal, linkString](
linkString, [linkMELowercase, linkMEOriginal, linkString] QString tooltipText, Link originalLink) {
(QString tooltipText, Link originalLink) { if (!tooltipText.isEmpty()) {
if (!tooltipText.isEmpty()) { linkMELowercase->setTooltip(tooltipText);
linkMELowercase->setTooltip(tooltipText); linkMEOriginal->setTooltip(tooltipText);
linkMEOriginal->setTooltip(tooltipText); }
} if (originalLink.value != linkString && !originalLink.value.isEmpty()) {
if (originalLink.value != linkString && linkMELowercase->setLink(originalLink)->updateLink();
!originalLink.value.isEmpty()) { linkMEOriginal->setLink(originalLink)->updateLink();
linkMELowercase->setLink(originalLink)->updateLink(); }
linkMEOriginal->setLink(originalLink)->updateLink(); });
}
});
} }
// if (!linkString.isEmpty()) { // if (!linkString.isEmpty()) {
@ -400,11 +558,9 @@ void TwitchMessageBuilder::appendUsername()
auto iterator = this->tags.find("display-name"); auto iterator = this->tags.find("display-name");
if (iterator != this->tags.end()) { if (iterator != this->tags.end()) {
QString displayName = QString displayName = parseTagString(iterator.value().toString()).trimmed();
parseTagString(iterator.value().toString()).trimmed();
if (QString::compare(displayName, this->userName, if (QString::compare(displayName, this->userName, Qt::CaseInsensitive) == 0) {
Qt::CaseInsensitive) == 0) {
username = displayName; username = displayName;
this->message().displayName = displayName; this->message().displayName = displayName;
@ -422,8 +578,7 @@ void TwitchMessageBuilder::appendUsername()
QString usernameText; QString usernameText;
pajlada::Settings::Setting<int> usernameDisplayMode( pajlada::Settings::Setting<int> usernameDisplayMode(
"/appearance/messages/usernameDisplayMode", "/appearance/messages/usernameDisplayMode", UsernameDisplayMode::UsernameAndLocalizedName);
UsernameDisplayMode::UsernameAndLocalizedName);
switch (usernameDisplayMode.getValue()) { switch (usernameDisplayMode.getValue()) {
case UsernameDisplayMode::Username: { case UsernameDisplayMode::Username: {
@ -454,8 +609,7 @@ void TwitchMessageBuilder::appendUsername()
// IrcManager::getInstance().getUser().getUserName(); // IrcManager::getInstance().getUser().getUserName();
} else if (this->args.isReceivedWhisper) { } else if (this->args.isReceivedWhisper) {
// Sender username // Sender username
this->emplace<TextElement>(usernameText, MessageElementFlag::Username, this->emplace<TextElement>(usernameText, MessageElementFlag::Username, this->usernameColor_,
this->usernameColor_,
FontStyle::ChatMediumBold) FontStyle::ChatMediumBold)
->setLink({Link::UserWhisper, this->message().displayName}); ->setLink({Link::UserWhisper, this->message().displayName});
@ -463,8 +617,7 @@ void TwitchMessageBuilder::appendUsername()
// Separator // Separator
this->emplace<TextElement>("->", MessageElementFlag::Username, this->emplace<TextElement>("->", MessageElementFlag::Username,
app->themes->messages.textColors.system, app->themes->messages.textColors.system, FontStyle::ChatMedium);
FontStyle::ChatMedium);
QColor selfColor = currentUser->color(); QColor selfColor = currentUser->color();
if (!selfColor.isValid()) { if (!selfColor.isValid()) {
@ -472,16 +625,14 @@ void TwitchMessageBuilder::appendUsername()
} }
// Your own username // Your own username
this->emplace<TextElement>(currentUser->getUserName() + ":", this->emplace<TextElement>(currentUser->getUserName() + ":", MessageElementFlag::Username,
MessageElementFlag::Username, selfColor, selfColor, FontStyle::ChatMediumBold);
FontStyle::ChatMediumBold);
} else { } else {
if (!this->action_) { if (!this->action_) {
usernameText += ":"; usernameText += ":";
} }
this->emplace<TextElement>(usernameText, MessageElementFlag::Username, this->emplace<TextElement>(usernameText, MessageElementFlag::Username, this->usernameColor_,
this->usernameColor_,
FontStyle::ChatMediumBold) FontStyle::ChatMediumBold)
->setLink({Link::UserInfo, this->message().displayName}); ->setLink({Link::UserInfo, this->message().displayName});
} }
@ -507,8 +658,7 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg)
// update the media player url if necessary // update the media player url if necessary
QUrl highlightSoundUrl; QUrl highlightSoundUrl;
if (getSettings()->customHighlightSound) { if (getSettings()->customHighlightSound) {
highlightSoundUrl = highlightSoundUrl = QUrl::fromLocalFile(getSettings()->pathHighlightSound.getValue());
QUrl::fromLocalFile(getSettings()->pathHighlightSound.getValue());
} else { } else {
highlightSoundUrl = QUrl("qrc:/sounds/ping2.wav"); highlightSoundUrl = QUrl("qrc:/sounds/ping2.wav");
} }
@ -521,15 +671,12 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg)
// TODO: This vector should only be rebuilt upon highlights being changed // TODO: This vector should only be rebuilt upon highlights being changed
// fourtf: should be implemented in the HighlightsController // fourtf: should be implemented in the HighlightsController
std::vector<HighlightPhrase> activeHighlights = std::vector<HighlightPhrase> activeHighlights = app->highlights->phrases.getVector();
app->highlights->phrases.getVector(); std::vector<HighlightPhrase> userHighlights = app->highlights->highlightedUsers.getVector();
std::vector<HighlightPhrase> userHighlights =
app->highlights->highlightedUsers.getVector();
if (getSettings()->enableSelfHighlight && currentUsername.size() > 0) { if (getSettings()->enableSelfHighlight && currentUsername.size() > 0) {
HighlightPhrase selfHighlight( HighlightPhrase selfHighlight(currentUsername, getSettings()->enableSelfHighlightTaskbar,
currentUsername, getSettings()->enableSelfHighlightTaskbar, getSettings()->enableSelfHighlightSound, false);
getSettings()->enableSelfHighlightSound, false);
activeHighlights.emplace_back(std::move(selfHighlight)); activeHighlights.emplace_back(std::move(selfHighlight));
} }
@ -564,8 +711,7 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg)
} }
for (const HighlightPhrase &userHighlight : userHighlights) { for (const HighlightPhrase &userHighlight : userHighlights) {
if (userHighlight.isMatch(this->ircMessage->nick())) { if (userHighlight.isMatch(this->ircMessage->nick())) {
log("Highlight because user {} sent a message", log("Highlight because user {} sent a message", this->ircMessage->nick());
this->ircMessage->nick());
doHighlight = true; doHighlight = true;
if (userHighlight.getAlert()) { if (userHighlight.getAlert()) {
@ -583,8 +729,7 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg)
} }
} }
} }
if (this->args.isReceivedWhisper && if (this->args.isReceivedWhisper && getSettings()->enableWhisperHighlight) {
getSettings()->enableWhisperHighlight) {
if (getSettings()->enableWhisperHighlightTaskbar) { if (getSettings()->enableWhisperHighlightTaskbar) {
doAlert = true; doAlert = true;
} }
@ -596,22 +741,19 @@ void TwitchMessageBuilder::parseHighlights(bool isPastMsg)
this->message().flags.set(MessageFlag::Highlighted, doHighlight); this->message().flags.set(MessageFlag::Highlighted, doHighlight);
if (!isPastMsg) { if (!isPastMsg) {
if (playSound && if (playSound && (!hasFocus || getSettings()->highlightAlwaysPlaySound)) {
(!hasFocus || getSettings()->highlightAlwaysPlaySound)) {
player->play(); player->play();
} }
if (doAlert) { if (doAlert) {
QApplication::alert(getApp()->windows->getMainWindow().window(), QApplication::alert(getApp()->windows->getMainWindow().window(), 2500);
2500);
} }
} }
} }
} }
void TwitchMessageBuilder::appendTwitchEmote( void TwitchMessageBuilder::appendTwitchEmote(const QString &emote,
const QString &emote, std::vector<std::tuple<int, EmotePtr, EmoteName>> &vec)
std::vector<std::pair<int, EmotePtr>> &vec)
{ {
auto app = getApp(); auto app = getApp();
if (!emote.contains(':')) { if (!emote.contains(':')) {
@ -638,16 +780,17 @@ void TwitchMessageBuilder::appendTwitchEmote(
auto start = coords.at(0).toInt(); auto start = coords.at(0).toInt();
auto end = coords.at(1).toInt(); auto end = coords.at(1).toInt();
if (start >= end || start < 0 || if (start >= end || start < 0 || end > this->originalMessage_.length()) {
end > this->originalMessage_.length()) {
return; return;
} }
auto name = auto name = EmoteName{this->originalMessage_.mid(start, end - start + 1)};
EmoteName{this->originalMessage_.mid(start, end - start + 1)}; auto tup = std::tuple<int, EmotePtr, EmoteName>{
start, app->emotes->twitch.getOrCreateEmote(id, name), name};
vec.push_back(std::make_pair( if (std::get<1>(tup) == nullptr) {
start, app->emotes->twitch.getOrCreateEmote(id, name))); log("nullptr {}", std::get<2>(tup).string);
}
vec.push_back(std::move(tup));
} }
} }
@ -686,7 +829,8 @@ void TwitchMessageBuilder::appendTwitchBadges()
auto app = getApp(); auto app = getApp();
auto iterator = this->tags.find("badges"); auto iterator = this->tags.find("badges");
if (iterator == this->tags.end()) return; if (iterator == this->tags.end())
return;
for (QString badge : iterator.value().toString().split(',')) { for (QString badge : iterator.value().toString().split(',')) {
if (badge.startsWith("bits/")) { if (badge.startsWith("bits/")) {
@ -696,10 +840,8 @@ void TwitchMessageBuilder::appendTwitchBadges()
// Try to fetch channel-specific bit badge // Try to fetch channel-specific bit badge
try { try {
if (twitchChannel) if (twitchChannel)
if (const auto &badge = this->twitchChannel->twitchBadge( if (const auto &badge = this->twitchChannel->twitchBadge("bits", cheerAmount)) {
"bits", cheerAmount)) { this->emplace<EmoteElement>(badge.get(), MessageElementFlag::BadgeVanity)
this->emplace<EmoteElement>(
badge.get(), MessageElementFlag::BadgeVanity)
->setTooltip(tooltip); ->setTooltip(tooltip);
continue; continue;
} }
@ -708,55 +850,45 @@ void TwitchMessageBuilder::appendTwitchBadges()
} }
// Use default bit badge // Use default bit badge
if (auto badge = this->twitchChannel->globalTwitchBadges().badge( if (auto badge = this->twitchChannel->globalTwitchBadges().badge("bits", cheerAmount)) {
"bits", cheerAmount)) { this->emplace<EmoteElement>(badge.get(), MessageElementFlag::BadgeVanity)
this->emplace<EmoteElement>(badge.get(),
MessageElementFlag::BadgeVanity)
->setTooltip(tooltip); ->setTooltip(tooltip);
} }
} else if (badge == "staff/1") { } else if (badge == "staff/1") {
this->emplace<ImageElement>( this->emplace<ImageElement>(Image::fromPixmap(app->resources->twitch.staff),
Image::fromPixmap(app->resources->twitch.staff), MessageElementFlag::BadgeGlobalAuthority)
MessageElementFlag::BadgeGlobalAuthority)
->setTooltip("Twitch Staff"); ->setTooltip("Twitch Staff");
} else if (badge == "admin/1") { } else if (badge == "admin/1") {
this->emplace<ImageElement>( this->emplace<ImageElement>(Image::fromPixmap(app->resources->twitch.admin),
Image::fromPixmap(app->resources->twitch.admin), MessageElementFlag::BadgeGlobalAuthority)
MessageElementFlag::BadgeGlobalAuthority)
->setTooltip("Twitch Admin"); ->setTooltip("Twitch Admin");
} else if (badge == "global_mod/1") { } else if (badge == "global_mod/1") {
this->emplace<ImageElement>( this->emplace<ImageElement>(Image::fromPixmap(app->resources->twitch.globalmod),
Image::fromPixmap(app->resources->twitch.globalmod), MessageElementFlag::BadgeGlobalAuthority)
MessageElementFlag::BadgeGlobalAuthority)
->setTooltip("Twitch Global Moderator"); ->setTooltip("Twitch Global Moderator");
} else if (badge == "moderator/1") { } else if (badge == "moderator/1") {
// TODO: Implement custom FFZ moderator badge // TODO: Implement custom FFZ moderator badge
this->emplace<ImageElement>( this->emplace<ImageElement>(Image::fromPixmap(app->resources->twitch.moderator),
Image::fromPixmap(app->resources->twitch.moderator), MessageElementFlag::BadgeChannelAuthority)
MessageElementFlag::BadgeChannelAuthority)
->setTooltip("Twitch Channel Moderator"); ->setTooltip("Twitch Channel Moderator");
} else if (badge == "turbo/1") { } else if (badge == "turbo/1") {
this->emplace<ImageElement>( this->emplace<ImageElement>(Image::fromPixmap(app->resources->twitch.turbo),
Image::fromPixmap(app->resources->twitch.turbo), MessageElementFlag::BadgeGlobalAuthority)
MessageElementFlag::BadgeGlobalAuthority)
->setTooltip("Twitch Turbo Subscriber"); ->setTooltip("Twitch Turbo Subscriber");
} else if (badge == "broadcaster/1") { } else if (badge == "broadcaster/1") {
this->emplace<ImageElement>( this->emplace<ImageElement>(Image::fromPixmap(app->resources->twitch.broadcaster),
Image::fromPixmap(app->resources->twitch.broadcaster), MessageElementFlag::BadgeChannelAuthority)
MessageElementFlag::BadgeChannelAuthority)
->setTooltip("Twitch Broadcaster"); ->setTooltip("Twitch Broadcaster");
} else if (badge == "premium/1") { } else if (badge == "premium/1") {
this->emplace<ImageElement>( this->emplace<ImageElement>(Image::fromPixmap(app->resources->twitch.prime),
Image::fromPixmap(app->resources->twitch.prime), MessageElementFlag::BadgeVanity)
MessageElementFlag::BadgeVanity)
->setTooltip("Twitch Prime Subscriber"); ->setTooltip("Twitch Prime Subscriber");
} else if (badge.startsWith("partner/")) { } else if (badge.startsWith("partner/")) {
int index = badge.midRef(8).toInt(); int index = badge.midRef(8).toInt();
switch (index) { switch (index) {
case 1: { case 1: {
this->emplace<ImageElement>( this->emplace<ImageElement>(
Image::fromPixmap(app->resources->twitch.verified, Image::fromPixmap(app->resources->twitch.verified, 0.25),
0.25),
MessageElementFlag::BadgeVanity) MessageElementFlag::BadgeVanity)
->setTooltip("Twitch Verified"); ->setTooltip("Twitch Verified");
} break; } break;
@ -767,27 +899,23 @@ void TwitchMessageBuilder::appendTwitchBadges()
} break; } break;
} }
} else if (badge.startsWith("subscriber/")) { } else if (badge.startsWith("subscriber/")) {
if (auto badgeEmote = this->twitchChannel->twitchBadge( if (auto badgeEmote = this->twitchChannel->twitchBadge("subscriber", badge.mid(11))) {
"subscriber", badge.mid(11))) { this->emplace<EmoteElement>(badgeEmote.get(), MessageElementFlag::BadgeSubscription)
this->emplace<EmoteElement>(
badgeEmote.get(), MessageElementFlag::BadgeSubscription)
->setTooltip((*badgeEmote)->tooltip.string); ->setTooltip((*badgeEmote)->tooltip.string);
continue; continue;
} }
// use default subscriber badge if custom one not found // use default subscriber badge if custom one not found
this->emplace<ImageElement>( this->emplace<ImageElement>(Image::fromPixmap(app->resources->twitch.subscriber, 0.25),
Image::fromPixmap(app->resources->twitch.subscriber, 0.25), MessageElementFlag::BadgeSubscription)
MessageElementFlag::BadgeSubscription)
->setTooltip("Twitch Subscriber"); ->setTooltip("Twitch Subscriber");
} else { } else {
auto splits = badge.split('/'); auto splits = badge.split('/');
if (splits.size() != 2) continue; if (splits.size() != 2)
continue;
if (auto badgeEmote = if (auto badgeEmote = this->twitchChannel->twitchBadge(splits[0], splits[1])) {
this->twitchChannel->twitchBadge(splits[0], splits[1])) { this->emplace<EmoteElement>(badgeEmote.get(), MessageElementFlag::BadgeVanity)
this->emplace<EmoteElement>(badgeEmote.get(),
MessageElementFlag::BadgeVanity)
->setTooltip((*badgeEmote)->tooltip.string); ->setTooltip((*badgeEmote)->tooltip.string);
continue; continue;
} }
@ -797,11 +925,9 @@ void TwitchMessageBuilder::appendTwitchBadges()
void TwitchMessageBuilder::appendChatterinoBadges() void TwitchMessageBuilder::appendChatterinoBadges()
{ {
auto chatterinoBadgePtr = auto chatterinoBadgePtr = getApp()->chatterinoBadges->getBadge({this->userName});
getApp()->chatterinoBadges->getBadge({this->userName});
if (chatterinoBadgePtr) { if (chatterinoBadgePtr) {
this->emplace<EmoteElement>(*chatterinoBadgePtr, this->emplace<EmoteElement>(*chatterinoBadgePtr, MessageElementFlag::BadgeChatterino);
MessageElementFlag::BadgeChatterino);
} }
} }

View file

@ -27,13 +27,10 @@ public:
TwitchMessageBuilder() = delete; TwitchMessageBuilder() = delete;
explicit TwitchMessageBuilder(Channel *_channel, explicit TwitchMessageBuilder(Channel *_channel, const Communi::IrcPrivateMessage *_ircMessage,
const Communi::IrcPrivateMessage *_ircMessage,
const MessageParseArgs &_args); const MessageParseArgs &_args);
explicit TwitchMessageBuilder(Channel *_channel, explicit TwitchMessageBuilder(Channel *_channel, const Communi::IrcMessage *_ircMessage,
const Communi::IrcMessage *_ircMessage, const MessageParseArgs &_args, QString content, bool isAction);
const MessageParseArgs &_args,
QString content, bool isAction);
Channel *channel; Channel *channel;
TwitchChannel *twitchChannel; TwitchChannel *twitchChannel;
@ -56,11 +53,11 @@ private:
void parseHighlights(bool isPastMsg); void parseHighlights(bool isPastMsg);
void appendTwitchEmote(const QString &emote, void appendTwitchEmote(const QString &emote,
std::vector<std::pair<int, EmotePtr>> &vec); std::vector<std::tuple<int, EmotePtr, EmoteName>> &vec);
Outcome tryAppendEmote(const EmoteName &name); Outcome tryAppendEmote(const EmoteName &name);
void addWords(const QStringList &words, void addWords(const QStringList &words,
const std::vector<std::pair<int, EmotePtr>> &twitchEmotes); const std::vector<std::tuple<int, EmotePtr, EmoteName>> &twitchEmotes);
void addTextOrEmoji(EmotePtr emote); void addTextOrEmoji(EmotePtr emote);
void addTextOrEmoji(const QString &value); void addTextOrEmoji(const QString &value);
@ -72,7 +69,7 @@ private:
bool hasBits_ = false; bool hasBits_ = false;
QColor usernameColor_; QColor usernameColor_;
const QString originalMessage_; QString originalMessage_;
bool senderIsBroadcaster{}; bool senderIsBroadcaster{};
const bool action_ = false; const bool action_ = false;

View file

@ -116,6 +116,9 @@ public:
BoolSetting enableUnshortLinks = {"/links/unshortLinks", false}; BoolSetting enableUnshortLinks = {"/links/unshortLinks", false};
BoolSetting enableLowercaseLink = {"/links/linkLowercase", true}; BoolSetting enableLowercaseLink = {"/links/linkLowercase", true};
/// Ignored phrases
QStringSetting ignoredPhraseReplace = {"/ignore/ignoredPhraseReplace", "***"};
/// Ingored Users /// Ingored Users
BoolSetting enableTwitchIgnoredUsers = {"/ignore/enableTwitchIgnoredUsers", BoolSetting enableTwitchIgnoredUsers = {"/ignore/enableTwitchIgnoredUsers",
true}; true};

View file

@ -25,8 +25,7 @@
namespace chatterino { namespace chatterino {
static void addPhrasesTab(LayoutCreator<QVBoxLayout> box); static void addPhrasesTab(LayoutCreator<QVBoxLayout> box);
static void addUsersTab(IgnoresPage &page, LayoutCreator<QVBoxLayout> box, static void addUsersTab(IgnoresPage &page, LayoutCreator<QVBoxLayout> box, QStringListModel &model);
QStringListModel &model);
IgnoresPage::IgnoresPage() IgnoresPage::IgnoresPage()
: SettingsPage("Ignores", "") : SettingsPage("Ignores", "")
@ -36,8 +35,7 @@ IgnoresPage::IgnoresPage()
auto tabs = layout.emplace<QTabWidget>(); auto tabs = layout.emplace<QTabWidget>();
addPhrasesTab(tabs.appendTab(new QVBoxLayout, "Phrases")); addPhrasesTab(tabs.appendTab(new QVBoxLayout, "Phrases"));
addUsersTab(*this, tabs.appendTab(new QVBoxLayout, "Users"), addUsersTab(*this, tabs.appendTab(new QVBoxLayout, "Users"), this->userListModel_);
this->userListModel_);
auto label = layout.emplace<QLabel>(INFO); auto label = layout.emplace<QLabel>(INFO);
label->setWordWrap(true); label->setWordWrap(true);
@ -47,14 +45,10 @@ IgnoresPage::IgnoresPage()
void addPhrasesTab(LayoutCreator<QVBoxLayout> layout) void addPhrasesTab(LayoutCreator<QVBoxLayout> layout)
{ {
EditableModelView *view = EditableModelView *view =
layout layout.emplace<EditableModelView>(getApp()->ignores->createModel(nullptr)).getElement();
.emplace<EditableModelView>(getApp()->ignores->createModel(nullptr)) view->setTitles({"Pattern", "Regex", "Case Sensitive", "Block", "Replacement"});
.getElement(); view->getTableView()->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);
view->setTitles({"Pattern", "Regex"}); view->getTableView()->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
view->getTableView()->horizontalHeader()->setSectionResizeMode(
QHeaderView::Fixed);
view->getTableView()->horizontalHeader()->setSectionResizeMode(
0, QHeaderView::Stretch);
QTimer::singleShot(1, [view] { QTimer::singleShot(1, [view] {
view->getTableView()->resizeColumnsToContents(); view->getTableView()->resizeColumnsToContents();
@ -62,12 +56,12 @@ void addPhrasesTab(LayoutCreator<QVBoxLayout> layout)
}); });
view->addButtonPressed.connect([] { view->addButtonPressed.connect([] {
getApp()->ignores->phrases.appendItem(IgnorePhrase{"my phrase", false}); getApp()->ignores->phrases.appendItem(IgnorePhrase{
"my phrase", false, false, getSettings()->ignoredPhraseReplace.getValue(), true});
}); });
} }
void addUsersTab(IgnoresPage &page, LayoutCreator<QVBoxLayout> users, void addUsersTab(IgnoresPage &page, LayoutCreator<QVBoxLayout> users, QStringListModel &userModel)
QStringListModel &userModel)
{ {
users.append(page.createCheckBox("Enable twitch ignored users", users.append(page.createCheckBox("Enable twitch ignored users",
getSettings()->enableTwitchIgnoredUsers)); getSettings()->enableTwitchIgnoredUsers));