Advanced channel filters (#1748)
Adds custom channel filters complete with their own mini-language. Filters can be created in settings, and applied by clicking the three dots to open the Split menu and selecting "Set filters".
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
#include "FilterModel.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
#include "util/StandardItemHelper.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
// commandmodel
|
||||
FilterModel::FilterModel(QObject *parent)
|
||||
: SignalVectorModel<FilterRecordPtr>(3, parent)
|
||||
{
|
||||
}
|
||||
|
||||
// turn a vector item into a model row
|
||||
FilterRecordPtr FilterModel::getItemFromRow(std::vector<QStandardItem *> &row,
|
||||
const FilterRecordPtr &original)
|
||||
{
|
||||
auto item =
|
||||
std::make_shared<FilterRecord>(row[0]->data(Qt::DisplayRole).toString(),
|
||||
row[1]->data(Qt::DisplayRole).toString(),
|
||||
original->getId()); // persist id
|
||||
|
||||
// force 'valid' column to update
|
||||
setBoolItem(row[2], item->valid(), false, false);
|
||||
setStringItem(row[2], item->valid() ? "Valid" : "Show errors");
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// turns a row in the model into a vector item
|
||||
void FilterModel::getRowFromItem(const FilterRecordPtr &item,
|
||||
std::vector<QStandardItem *> &row)
|
||||
{
|
||||
setStringItem(row[0], item->getName());
|
||||
setStringItem(row[1], item->getFilter());
|
||||
setBoolItem(row[2], item->valid(), false, false);
|
||||
setStringItem(row[2], item->valid() ? "Valid" : "Show errors");
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "common/SignalVectorModel.hpp"
|
||||
#include "controllers/filters/FilterRecord.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class FilterModel : public SignalVectorModel<FilterRecordPtr>
|
||||
{
|
||||
public:
|
||||
explicit FilterModel(QObject *parent);
|
||||
|
||||
protected:
|
||||
// turn a vector item into a model row
|
||||
virtual FilterRecordPtr getItemFromRow(
|
||||
std::vector<QStandardItem *> &row,
|
||||
const FilterRecordPtr &original) override;
|
||||
|
||||
// turns a row in the model into a vector item
|
||||
virtual void getRowFromItem(const FilterRecordPtr &item,
|
||||
std::vector<QStandardItem *> &row) override;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include "util/RapidJsonSerializeQString.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
|
||||
#include "controllers/filters/parser/FilterParser.hpp"
|
||||
#include "controllers/filters/parser/Types.hpp"
|
||||
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
#include <pajlada/serialize.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class FilterRecord
|
||||
{
|
||||
public:
|
||||
bool operator==(const FilterRecord &other) const
|
||||
{
|
||||
return std::tie(this->name_, this->filter_, this->id_) ==
|
||||
std::tie(other.name_, other.filter_, other.id_);
|
||||
}
|
||||
|
||||
FilterRecord(const QString &name, const QString &filter)
|
||||
: name_(name)
|
||||
, filter_(filter)
|
||||
, id_(QUuid::createUuid())
|
||||
, parser_(std::make_unique<filterparser::FilterParser>(filter))
|
||||
{
|
||||
}
|
||||
|
||||
FilterRecord(const QString &name, const QString &filter, const QUuid &id)
|
||||
: name_(name)
|
||||
, filter_(filter)
|
||||
, id_(id)
|
||||
, parser_(std::make_unique<filterparser::FilterParser>(filter))
|
||||
{
|
||||
}
|
||||
|
||||
const QString &getName() const
|
||||
{
|
||||
return this->name_;
|
||||
}
|
||||
|
||||
const QString &getFilter() const
|
||||
{
|
||||
return this->filter_;
|
||||
}
|
||||
|
||||
const QUuid &getId() const
|
||||
{
|
||||
return this->id_;
|
||||
}
|
||||
|
||||
bool valid() const
|
||||
{
|
||||
return this->parser_->valid();
|
||||
}
|
||||
|
||||
bool filter(const MessagePtr &message) const
|
||||
{
|
||||
return this->parser_->execute(message);
|
||||
}
|
||||
|
||||
bool filter(const filterparser::ContextMap &context) const
|
||||
{
|
||||
return this->parser_->execute(context);
|
||||
}
|
||||
|
||||
private:
|
||||
QString name_;
|
||||
QString filter_;
|
||||
QUuid id_;
|
||||
|
||||
std::unique_ptr<filterparser::FilterParser> parser_;
|
||||
};
|
||||
|
||||
using FilterRecordPtr = std::shared_ptr<FilterRecord>;
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
namespace pajlada {
|
||||
|
||||
template <>
|
||||
struct Serialize<chatterino::FilterRecordPtr> {
|
||||
static rapidjson::Value get(const chatterino::FilterRecordPtr &value,
|
||||
rapidjson::Document::AllocatorType &a)
|
||||
{
|
||||
rapidjson::Value ret(rapidjson::kObjectType);
|
||||
|
||||
chatterino::rj::set(ret, "name", value->getName(), a);
|
||||
chatterino::rj::set(ret, "filter", value->getFilter(), a);
|
||||
chatterino::rj::set(ret, "id",
|
||||
value->getId().toString(QUuid::WithoutBraces), a);
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Deserialize<chatterino::FilterRecordPtr> {
|
||||
static chatterino::FilterRecordPtr get(const rapidjson::Value &value)
|
||||
{
|
||||
if (!value.IsObject())
|
||||
{
|
||||
return std::make_shared<chatterino::FilterRecord>(QString(),
|
||||
QString());
|
||||
}
|
||||
|
||||
QString _name, _filter, _id;
|
||||
|
||||
chatterino::rj::getSafe(value, "name", _name);
|
||||
chatterino::rj::getSafe(value, "filter", _filter);
|
||||
chatterino::rj::getSafe(value, "id", _id);
|
||||
|
||||
return std::make_shared<chatterino::FilterRecord>(
|
||||
_name, _filter, QUuid::fromString(_id));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pajlada
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "controllers/filters/FilterRecord.hpp"
|
||||
#include "singletons/Settings.hpp"
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class FilterSet
|
||||
{
|
||||
public:
|
||||
FilterSet()
|
||||
{
|
||||
this->listener_ =
|
||||
getCSettings().filterRecords.delayedItemsChanged.connect(
|
||||
[this] { this->reloadFilters(); });
|
||||
}
|
||||
|
||||
FilterSet(const QList<QUuid> &filterIds)
|
||||
{
|
||||
auto filters = getCSettings().filterRecords.readOnly();
|
||||
for (const auto &f : *filters)
|
||||
{
|
||||
if (filterIds.contains(f->getId()))
|
||||
this->filters_.insert(f->getId(), f);
|
||||
}
|
||||
|
||||
this->listener_ =
|
||||
getCSettings().filterRecords.delayedItemsChanged.connect(
|
||||
[this] { this->reloadFilters(); });
|
||||
}
|
||||
|
||||
~FilterSet()
|
||||
{
|
||||
this->listener_.disconnect();
|
||||
}
|
||||
|
||||
bool filter(const MessagePtr &m) const
|
||||
{
|
||||
if (this->filters_.size() == 0)
|
||||
return true;
|
||||
|
||||
filterparser::ContextMap context = filterparser::buildContextMap(m);
|
||||
for (const auto &f : this->filters_.values())
|
||||
{
|
||||
if (!f->valid() || !f->filter(context))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const QList<QUuid> filterIds() const
|
||||
{
|
||||
return this->filters_.keys();
|
||||
}
|
||||
|
||||
private:
|
||||
QMap<QUuid, FilterRecordPtr> filters_;
|
||||
pajlada::Signals::Connection listener_;
|
||||
|
||||
void reloadFilters()
|
||||
{
|
||||
auto filters = getCSettings().filterRecords.readOnly();
|
||||
for (const auto &key : this->filters_.keys())
|
||||
{
|
||||
bool found = false;
|
||||
for (const auto &f : *filters)
|
||||
{
|
||||
if (f->getId() == key)
|
||||
{
|
||||
found = true;
|
||||
this->filters_.insert(key, f);
|
||||
}
|
||||
}
|
||||
if (!found)
|
||||
{
|
||||
this->filters_.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
using FilterSetPtr = std::shared_ptr<FilterSet>;
|
||||
|
||||
} // namespace chatterino
|
||||
@@ -0,0 +1,302 @@
|
||||
#include "FilterParser.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "controllers/filters/parser/Types.hpp"
|
||||
#include "providers/twitch/TwitchIrcServer.hpp"
|
||||
|
||||
namespace filterparser {
|
||||
|
||||
ContextMap buildContextMap(const MessagePtr &m)
|
||||
{
|
||||
auto watchingChannel =
|
||||
chatterino::getApp()->twitch.server->watchingChannel.get();
|
||||
|
||||
/* Known Identifiers
|
||||
*
|
||||
* author.badges
|
||||
* author.color
|
||||
* author.name
|
||||
* author.no_color
|
||||
* author.subbed
|
||||
* author.sub_length
|
||||
*
|
||||
* channel.name
|
||||
* channel.watching
|
||||
*
|
||||
* flags.highlighted
|
||||
* flags.points_redeemed
|
||||
* flags.sub_message
|
||||
* flags.system_message
|
||||
* flags.whisper
|
||||
*
|
||||
* message.content
|
||||
* message.length
|
||||
*
|
||||
*/
|
||||
|
||||
using MessageFlag = chatterino::MessageFlag;
|
||||
|
||||
QStringList badges;
|
||||
badges.reserve(m->badges.size());
|
||||
for (const auto &e : m->badges)
|
||||
{
|
||||
badges << e.key_;
|
||||
}
|
||||
|
||||
bool watching = !watchingChannel->getName().isEmpty() &&
|
||||
watchingChannel->getName().compare(
|
||||
m->channelName, Qt::CaseInsensitive) == 0;
|
||||
|
||||
bool subscribed = badges.contains("subscriber");
|
||||
int subLength = (subscribed && m->badgeInfos.count("subscriber") != 0)
|
||||
? m->badgeInfos.at("subscriber").toInt()
|
||||
: 0;
|
||||
|
||||
return {
|
||||
{"author.badges", std::move(badges)},
|
||||
{"author.color", m->usernameColor},
|
||||
{"author.name", m->displayName},
|
||||
{"author.no_color", !m->usernameColor.isValid()},
|
||||
{"author.subbed", subscribed},
|
||||
{"author.sub_length", subLength},
|
||||
|
||||
{"channel.name", m->channelName},
|
||||
{"channel.watching", watching},
|
||||
|
||||
{"flags.highlighted", m->flags.has(MessageFlag::Highlighted)},
|
||||
{"flags.points_redeemed", m->flags.has(MessageFlag::RedeemedHighlight)},
|
||||
{"flags.sub_message", m->flags.has(MessageFlag::Subscription)},
|
||||
{"flags.system_message", m->flags.has(MessageFlag::System)},
|
||||
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
|
||||
|
||||
{"message.content", m->messageText},
|
||||
{"message.length", m->messageText.length()},
|
||||
};
|
||||
}
|
||||
|
||||
FilterParser::FilterParser(const QString &text)
|
||||
: text_(text)
|
||||
, tokenizer_(Tokenizer(text))
|
||||
, builtExpression_(this->parseExpression(true))
|
||||
{
|
||||
}
|
||||
|
||||
bool FilterParser::execute(const MessagePtr &message) const
|
||||
{
|
||||
auto context = buildContextMap(message);
|
||||
return this->execute(context);
|
||||
}
|
||||
|
||||
bool FilterParser::execute(const ContextMap &context) const
|
||||
{
|
||||
return this->builtExpression_->execute(context).toBool();
|
||||
}
|
||||
|
||||
bool FilterParser::valid() const
|
||||
{
|
||||
return this->valid_;
|
||||
}
|
||||
|
||||
ExpressionPtr FilterParser::parseExpression(bool top)
|
||||
{
|
||||
auto e = this->parseAnd();
|
||||
while (this->tokenizer_.hasNext() &&
|
||||
this->tokenizer_.nextTokenType() == TokenType::OR)
|
||||
{
|
||||
this->tokenizer_.next();
|
||||
auto nextAnd = this->parseAnd();
|
||||
e = std::make_unique<BinaryOperation>(TokenType::OR, std::move(e),
|
||||
std::move(nextAnd));
|
||||
}
|
||||
|
||||
if (this->tokenizer_.hasNext() && top)
|
||||
{
|
||||
this->errorLog(QString("Unexpected token at end: %1")
|
||||
.arg(this->tokenizer_.preview()));
|
||||
}
|
||||
|
||||
return e;
|
||||
}
|
||||
|
||||
ExpressionPtr FilterParser::parseAnd()
|
||||
{
|
||||
auto e = this->parseUnary();
|
||||
while (this->tokenizer_.hasNext() &&
|
||||
this->tokenizer_.nextTokenType() == TokenType::AND)
|
||||
{
|
||||
this->tokenizer_.next();
|
||||
auto nextUnary = this->parseUnary();
|
||||
e = std::make_unique<BinaryOperation>(TokenType::AND, std::move(e),
|
||||
std::move(nextUnary));
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
ExpressionPtr FilterParser::parseUnary()
|
||||
{
|
||||
if (this->tokenizer_.hasNext() && this->tokenizer_.nextTokenIsUnaryOp())
|
||||
{
|
||||
this->tokenizer_.next();
|
||||
auto type = this->tokenizer_.tokenType();
|
||||
auto nextCondition = this->parseCondition();
|
||||
return std::make_unique<UnaryOperation>(type, std::move(nextCondition));
|
||||
}
|
||||
else
|
||||
{
|
||||
return this->parseCondition();
|
||||
}
|
||||
}
|
||||
|
||||
ExpressionPtr FilterParser::parseParentheses()
|
||||
{
|
||||
// Don't call .next() before calling this method
|
||||
assert(this->tokenizer_.nextTokenType() == TokenType::LP);
|
||||
|
||||
this->tokenizer_.next();
|
||||
auto e = this->parseExpression();
|
||||
if (this->tokenizer_.hasNext() &&
|
||||
this->tokenizer_.nextTokenType() == TokenType::RP)
|
||||
{
|
||||
this->tokenizer_.next();
|
||||
return e;
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto message =
|
||||
this->tokenizer_.hasNext()
|
||||
? QString("Missing closing parentheses: got %1")
|
||||
.arg(this->tokenizer_.preview())
|
||||
: "Missing closing parentheses at end of statement";
|
||||
this->errorLog(message);
|
||||
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
ExpressionPtr FilterParser::parseCondition()
|
||||
{
|
||||
ExpressionPtr value;
|
||||
// parse expression wrapped in parentheses
|
||||
if (this->tokenizer_.hasNext() &&
|
||||
this->tokenizer_.nextTokenType() == TokenType::LP)
|
||||
{
|
||||
// get value inside parentheses
|
||||
value = this->parseParentheses();
|
||||
}
|
||||
else
|
||||
{
|
||||
// get current value
|
||||
value = this->parseValue();
|
||||
}
|
||||
|
||||
// expecting an operator or nothing
|
||||
while (this->tokenizer_.hasNext())
|
||||
{
|
||||
if (this->tokenizer_.nextTokenIsBinaryOp())
|
||||
{
|
||||
this->tokenizer_.next();
|
||||
auto type = this->tokenizer_.tokenType();
|
||||
auto nextValue = this->parseValue();
|
||||
return std::make_unique<BinaryOperation>(type, std::move(value),
|
||||
std::move(nextValue));
|
||||
}
|
||||
else if (this->tokenizer_.nextTokenIsMathOp())
|
||||
{
|
||||
this->tokenizer_.next();
|
||||
auto type = this->tokenizer_.tokenType();
|
||||
auto nextValue = this->parseValue();
|
||||
value = std::make_unique<BinaryOperation>(type, std::move(value),
|
||||
std::move(nextValue));
|
||||
}
|
||||
else if (this->tokenizer_.nextTokenType() == TokenType::RP)
|
||||
{
|
||||
// RP, so move on
|
||||
break;
|
||||
}
|
||||
else if (!this->tokenizer_.nextTokenIsOp())
|
||||
{
|
||||
this->errorLog(QString("Expected an operator but got %1 %2")
|
||||
.arg(this->tokenizer_.preview())
|
||||
.arg(tokenTypeToInfoString(
|
||||
this->tokenizer_.nextTokenType())));
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
ExpressionPtr FilterParser::parseValue()
|
||||
{
|
||||
// parse a literal or an expression wrapped in parenthsis
|
||||
if (this->tokenizer_.hasNext())
|
||||
{
|
||||
auto type = this->tokenizer_.nextTokenType();
|
||||
if (type == TokenType::INT)
|
||||
{
|
||||
return std::make_unique<ValueExpression>(
|
||||
this->tokenizer_.next().toInt(), type);
|
||||
}
|
||||
else if (type == TokenType::STRING)
|
||||
{
|
||||
auto before = this->tokenizer_.next();
|
||||
// remove quote marks
|
||||
auto val = before.mid(1);
|
||||
val.chop(1);
|
||||
val = val.replace("\\\"", "\"");
|
||||
return std::make_unique<ValueExpression>(val, type);
|
||||
}
|
||||
else if (type == TokenType::IDENTIFIER)
|
||||
{
|
||||
return std::make_unique<ValueExpression>(this->tokenizer_.next(),
|
||||
type);
|
||||
}
|
||||
else if (type == TokenType::LP)
|
||||
{
|
||||
return this->parseParentheses();
|
||||
}
|
||||
else
|
||||
{
|
||||
this->tokenizer_.next();
|
||||
this->errorLog(QString("Expected value but got %1 %2")
|
||||
.arg(this->tokenizer_.current())
|
||||
.arg(tokenTypeToInfoString(type)));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this->errorLog("Unexpected end of statement");
|
||||
}
|
||||
|
||||
return std::make_unique<ValueExpression>(0, TokenType::INT);
|
||||
}
|
||||
|
||||
void FilterParser::errorLog(const QString &text, bool expand)
|
||||
{
|
||||
this->valid_ = false;
|
||||
if (expand || this->parseLog_.size() == 0)
|
||||
{
|
||||
this->parseLog_ << text;
|
||||
}
|
||||
}
|
||||
|
||||
const QStringList &FilterParser::errors() const
|
||||
{
|
||||
return this->parseLog_;
|
||||
}
|
||||
|
||||
const QString FilterParser::debugString() const
|
||||
{
|
||||
return this->builtExpression_->debug();
|
||||
}
|
||||
|
||||
const QString FilterParser::filterString() const
|
||||
{
|
||||
return this->builtExpression_->filterString();
|
||||
}
|
||||
|
||||
} // namespace filterparser
|
||||
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "controllers/filters/parser/Tokenizer.hpp"
|
||||
#include "controllers/filters/parser/Types.hpp"
|
||||
|
||||
namespace filterparser {
|
||||
|
||||
ContextMap buildContextMap(const MessagePtr &m);
|
||||
|
||||
class FilterParser
|
||||
{
|
||||
public:
|
||||
FilterParser(const QString &text);
|
||||
bool execute(const MessagePtr &message) const;
|
||||
bool execute(const ContextMap &context) const;
|
||||
bool valid() const;
|
||||
|
||||
const QStringList &errors() const;
|
||||
const QString debugString() const;
|
||||
const QString filterString() const;
|
||||
|
||||
private:
|
||||
ExpressionPtr parseExpression(bool top = false);
|
||||
ExpressionPtr parseAnd();
|
||||
ExpressionPtr parseUnary();
|
||||
ExpressionPtr parseParentheses();
|
||||
ExpressionPtr parseCondition();
|
||||
ExpressionPtr parseValue();
|
||||
|
||||
void errorLog(const QString &text, bool expand = false);
|
||||
|
||||
QStringList parseLog_;
|
||||
bool valid_ = true;
|
||||
|
||||
QString text_;
|
||||
Tokenizer tokenizer_;
|
||||
ExpressionPtr builtExpression_;
|
||||
};
|
||||
} // namespace filterparser
|
||||
@@ -0,0 +1,174 @@
|
||||
#include "controllers/filters/parser/Tokenizer.hpp"
|
||||
|
||||
namespace filterparser {
|
||||
|
||||
Tokenizer::Tokenizer(const QString &text)
|
||||
{
|
||||
QRegularExpressionMatchIterator i = tokenRegex.globalMatch(text);
|
||||
while (i.hasNext())
|
||||
{
|
||||
auto text = i.next().captured();
|
||||
this->tokens_ << text;
|
||||
this->tokenTypes_ << this->tokenize(text);
|
||||
}
|
||||
}
|
||||
|
||||
bool Tokenizer::hasNext() const
|
||||
{
|
||||
return this->i_ < this->tokens_.length();
|
||||
}
|
||||
|
||||
QString Tokenizer::next()
|
||||
{
|
||||
this->i_++;
|
||||
return this->tokens_.at(this->i_ - 1);
|
||||
}
|
||||
|
||||
QString Tokenizer::current() const
|
||||
{
|
||||
return this->tokens_.at(this->i_ - 1);
|
||||
}
|
||||
|
||||
QString Tokenizer::preview() const
|
||||
{
|
||||
if (this->hasNext())
|
||||
return this->tokens_.at(this->i_);
|
||||
return "";
|
||||
}
|
||||
|
||||
TokenType Tokenizer::nextTokenType() const
|
||||
{
|
||||
return this->tokenTypes_.at(this->i_);
|
||||
}
|
||||
|
||||
TokenType Tokenizer::tokenType() const
|
||||
{
|
||||
return this->tokenTypes_.at(this->i_ - 1);
|
||||
}
|
||||
|
||||
bool Tokenizer::nextTokenIsOp() const
|
||||
{
|
||||
return this->typeIsOp(this->nextTokenType());
|
||||
}
|
||||
|
||||
bool Tokenizer::nextTokenIsBinaryOp() const
|
||||
{
|
||||
return this->typeIsBinaryOp(this->nextTokenType());
|
||||
}
|
||||
|
||||
bool Tokenizer::nextTokenIsUnaryOp() const
|
||||
{
|
||||
return this->typeIsUnaryOp(this->nextTokenType());
|
||||
}
|
||||
|
||||
bool Tokenizer::nextTokenIsMathOp() const
|
||||
{
|
||||
return this->typeIsMathOp(this->nextTokenType());
|
||||
}
|
||||
|
||||
void Tokenizer::debug()
|
||||
{
|
||||
if (this->i_ > 0)
|
||||
{
|
||||
qDebug() << "= current" << this->tokens_.at(this->i_ - 1);
|
||||
qDebug() << "= current type" << this->tokenTypes_.at(this->i_ - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "= no current";
|
||||
}
|
||||
if (this->hasNext())
|
||||
{
|
||||
qDebug() << "= next" << this->tokens_.at(this->i_);
|
||||
qDebug() << "= next type" << this->tokenTypes_.at(this->i_);
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "= no next";
|
||||
}
|
||||
}
|
||||
|
||||
const QStringList Tokenizer::allTokens()
|
||||
{
|
||||
return this->tokens_;
|
||||
}
|
||||
|
||||
TokenType Tokenizer::tokenize(const QString &text)
|
||||
{
|
||||
if (text == "&&")
|
||||
return TokenType::AND;
|
||||
else if (text == "||")
|
||||
return TokenType::OR;
|
||||
else if (text == "(")
|
||||
return TokenType::LP;
|
||||
else if (text == ")")
|
||||
return TokenType::RP;
|
||||
else if (text == "+")
|
||||
return TokenType::PLUS;
|
||||
else if (text == "-")
|
||||
return TokenType::MINUS;
|
||||
else if (text == "*")
|
||||
return TokenType::MULTIPLY;
|
||||
else if (text == "/")
|
||||
return TokenType::DIVIDE;
|
||||
else if (text == "==")
|
||||
return TokenType::EQ;
|
||||
else if (text == "!=")
|
||||
return TokenType::NEQ;
|
||||
else if (text == "%")
|
||||
return TokenType::MOD;
|
||||
else if (text == "<")
|
||||
return TokenType::LT;
|
||||
else if (text == ">")
|
||||
return TokenType::GT;
|
||||
else if (text == "<=")
|
||||
return TokenType::LTE;
|
||||
else if (text == ">=")
|
||||
return TokenType::GTE;
|
||||
else if (text == "contains")
|
||||
return TokenType::CONTAINS;
|
||||
else if (text == "startswith")
|
||||
return TokenType::STARTS_WITH;
|
||||
else if (text == "endswith")
|
||||
return TokenType::ENDS_WITH;
|
||||
else if (text == "!")
|
||||
return TokenType::NOT;
|
||||
else
|
||||
{
|
||||
if (text.front() == '"' && text.back() == '"')
|
||||
return TokenType::STRING;
|
||||
|
||||
if (validIdentifiersMap.keys().contains(text))
|
||||
return TokenType::IDENTIFIER;
|
||||
|
||||
bool flag;
|
||||
if (text.toInt(&flag); flag)
|
||||
return TokenType::INT;
|
||||
}
|
||||
|
||||
return TokenType::NONE;
|
||||
}
|
||||
|
||||
bool Tokenizer::typeIsOp(TokenType token)
|
||||
{
|
||||
return typeIsBinaryOp(token) || typeIsUnaryOp(token) ||
|
||||
typeIsMathOp(token) || token == TokenType::AND ||
|
||||
token == TokenType::OR;
|
||||
}
|
||||
|
||||
bool Tokenizer::typeIsBinaryOp(TokenType token)
|
||||
{
|
||||
return token > TokenType::BINARY_START && token < TokenType::BINARY_END;
|
||||
}
|
||||
|
||||
bool Tokenizer::typeIsUnaryOp(TokenType token)
|
||||
{
|
||||
return token > TokenType::UNARY_START && token < TokenType::UNARY_END;
|
||||
}
|
||||
|
||||
bool Tokenizer::typeIsMathOp(TokenType token)
|
||||
{
|
||||
return token > TokenType::MATH_START && token < TokenType::MATH_END;
|
||||
}
|
||||
|
||||
} // namespace filterparser
|
||||
@@ -0,0 +1,65 @@
|
||||
#pragma once
|
||||
|
||||
#include "controllers/filters/parser/Types.hpp"
|
||||
|
||||
namespace filterparser {
|
||||
|
||||
static const QMap<QString, QString> validIdentifiersMap = {
|
||||
{"author.badges", "author badges"},
|
||||
{"author.color", "author color"},
|
||||
{"author.name", "author name"},
|
||||
{"author.no_color", "author has no color?"},
|
||||
{"author.subbed", "author subscribed?"},
|
||||
{"author.sub_length", "author sub length"},
|
||||
{"channel.name", "channel name"},
|
||||
{"channel.watching", "/watching channel?"},
|
||||
{"flags.highlighted", "highlighted?"},
|
||||
{"flags.points_redeemed", "redeemed points?"},
|
||||
{"flags.sub_message", "sub/resub message?"},
|
||||
{"flags.system_message", "system message?"},
|
||||
{"flags.whisper", "whisper message?"},
|
||||
{"message.content", "message text"},
|
||||
{"message.length", "message length"}};
|
||||
|
||||
// clang-format off
|
||||
static const QRegularExpression tokenRegex(
|
||||
QString("\\\"((\\\\\")|[^\\\"])*\\\"|") + // String literal
|
||||
QString("[\\w\\.]+|") + // Identifier or reserved keyword
|
||||
QString("(<=?|>=?|!=?|==|\\|\\||&&|\\+|-|\\*|\\/|%)+|") + // Operator
|
||||
QString("[\\(\\)]") // Parentheses
|
||||
);
|
||||
// clang-format on
|
||||
|
||||
class Tokenizer
|
||||
{
|
||||
public:
|
||||
Tokenizer(const QString &text);
|
||||
|
||||
bool hasNext() const;
|
||||
QString next();
|
||||
QString current() const;
|
||||
QString preview() const;
|
||||
TokenType nextTokenType() const;
|
||||
TokenType tokenType() const;
|
||||
|
||||
bool nextTokenIsOp() const;
|
||||
bool nextTokenIsBinaryOp() const;
|
||||
bool nextTokenIsUnaryOp() const;
|
||||
bool nextTokenIsMathOp() const;
|
||||
|
||||
void debug();
|
||||
const QStringList allTokens();
|
||||
|
||||
static bool typeIsOp(TokenType token);
|
||||
static bool typeIsBinaryOp(TokenType token);
|
||||
static bool typeIsUnaryOp(TokenType token);
|
||||
static bool typeIsMathOp(TokenType token);
|
||||
|
||||
private:
|
||||
int i_ = 0;
|
||||
QStringList tokens_;
|
||||
QList<TokenType> tokenTypes_;
|
||||
|
||||
TokenType tokenize(const QString &text);
|
||||
};
|
||||
} // namespace filterparser
|
||||
@@ -0,0 +1,363 @@
|
||||
#include "controllers/filters/parser/Types.hpp"
|
||||
|
||||
namespace filterparser {
|
||||
|
||||
bool convertVariantTypes(QVariant &a, QVariant &b, int type)
|
||||
{
|
||||
return a.convert(type) && b.convert(type);
|
||||
}
|
||||
|
||||
bool variantTypesMatch(QVariant &a, QVariant &b, QVariant::Type type)
|
||||
{
|
||||
return a.type() == type && b.type() == type;
|
||||
}
|
||||
|
||||
QString tokenTypeToInfoString(TokenType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case CONTROL_START:
|
||||
case CONTROL_END:
|
||||
case BINARY_START:
|
||||
case BINARY_END:
|
||||
case UNARY_START:
|
||||
case UNARY_END:
|
||||
case MATH_START:
|
||||
case MATH_END:
|
||||
case OTHER_START:
|
||||
case NONE:
|
||||
return "<unknown>";
|
||||
case AND:
|
||||
return "<and>";
|
||||
case OR:
|
||||
return "<or>";
|
||||
case LP:
|
||||
return "<left parenthesis>";
|
||||
case RP:
|
||||
return "<right parenthesis>";
|
||||
case PLUS:
|
||||
return "<plus>";
|
||||
case MINUS:
|
||||
return "<minus>";
|
||||
case MULTIPLY:
|
||||
return "<multiply>";
|
||||
case DIVIDE:
|
||||
return "<divide>";
|
||||
case MOD:
|
||||
return "<modulus>";
|
||||
case EQ:
|
||||
return "<equals>";
|
||||
case NEQ:
|
||||
return "<not equals>";
|
||||
case LT:
|
||||
return "<less than>";
|
||||
case GT:
|
||||
return "<greater than>";
|
||||
case LTE:
|
||||
return "<less than equal>";
|
||||
case GTE:
|
||||
return "<greater than equal>";
|
||||
case CONTAINS:
|
||||
return "<contains>";
|
||||
case STARTS_WITH:
|
||||
return "<starts with>";
|
||||
case ENDS_WITH:
|
||||
return "<ends with>";
|
||||
case NOT:
|
||||
return "<not>";
|
||||
case STRING:
|
||||
return "<string>";
|
||||
case INT:
|
||||
return "<int>";
|
||||
case IDENTIFIER:
|
||||
return "<identifier>";
|
||||
default:
|
||||
return "<unknown>";
|
||||
}
|
||||
}
|
||||
|
||||
// ValueExpression
|
||||
|
||||
ValueExpression::ValueExpression(QVariant value, TokenType type)
|
||||
: value_(value)
|
||||
, type_(type){};
|
||||
|
||||
QVariant ValueExpression::execute(const ContextMap &context) const
|
||||
{
|
||||
if (this->type_ == TokenType::IDENTIFIER)
|
||||
{
|
||||
return context.value(this->value_.toString());
|
||||
}
|
||||
return this->value_;
|
||||
}
|
||||
|
||||
TokenType ValueExpression::type()
|
||||
{
|
||||
return this->type_;
|
||||
}
|
||||
|
||||
QString ValueExpression::debug() const
|
||||
{
|
||||
return this->value_.toString();
|
||||
}
|
||||
|
||||
QString ValueExpression::filterString() const
|
||||
{
|
||||
switch (this->type_)
|
||||
{
|
||||
case INT:
|
||||
return QString::number(this->value_.toInt());
|
||||
case STRING:
|
||||
return QString("\"%1\"").arg(
|
||||
this->value_.toString().replace("\"", "\\\""));
|
||||
case IDENTIFIER:
|
||||
return this->value_.toString();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// BinaryOperation
|
||||
|
||||
BinaryOperation::BinaryOperation(TokenType op, ExpressionPtr left,
|
||||
ExpressionPtr right)
|
||||
: op_(op)
|
||||
, left_(std::move(left))
|
||||
, right_(std::move(right))
|
||||
{
|
||||
}
|
||||
|
||||
QVariant BinaryOperation::execute(const ContextMap &context) const
|
||||
{
|
||||
auto left = this->left_->execute(context);
|
||||
auto right = this->right_->execute(context);
|
||||
switch (this->op_)
|
||||
{
|
||||
case PLUS:
|
||||
if (left.type() == QVariant::Type::String &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
return left.toString().append(right.toString());
|
||||
}
|
||||
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||
{
|
||||
return left.toInt() + right.toInt();
|
||||
}
|
||||
return 0;
|
||||
case MINUS:
|
||||
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||
return left.toInt() - right.toInt();
|
||||
return 0;
|
||||
case MULTIPLY:
|
||||
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||
return left.toInt() * right.toInt();
|
||||
return 0;
|
||||
case DIVIDE:
|
||||
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||
return left.toInt() / right.toInt();
|
||||
return 0;
|
||||
case MOD:
|
||||
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||
return left.toInt() % right.toInt();
|
||||
return 0;
|
||||
case OR:
|
||||
if (convertVariantTypes(left, right, QMetaType::Bool))
|
||||
return left.toBool() || right.toBool();
|
||||
return false;
|
||||
case AND:
|
||||
if (convertVariantTypes(left, right, QMetaType::Bool))
|
||||
return left.toBool() && right.toBool();
|
||||
return false;
|
||||
case EQ:
|
||||
if (variantTypesMatch(left, right, QVariant::Type::String))
|
||||
{
|
||||
return left.toString().compare(right.toString(),
|
||||
Qt::CaseInsensitive) == 0;
|
||||
}
|
||||
return left == right;
|
||||
case NEQ:
|
||||
if (variantTypesMatch(left, right, QVariant::Type::String))
|
||||
{
|
||||
return left.toString().compare(right.toString(),
|
||||
Qt::CaseInsensitive) != 0;
|
||||
}
|
||||
return left != right;
|
||||
case LT:
|
||||
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||
return left.toInt() < right.toInt();
|
||||
return false;
|
||||
case GT:
|
||||
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||
return left.toInt() > right.toInt();
|
||||
return false;
|
||||
case LTE:
|
||||
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||
return left.toInt() <= right.toInt();
|
||||
return false;
|
||||
case GTE:
|
||||
if (convertVariantTypes(left, right, QMetaType::Int))
|
||||
return left.toInt() >= right.toInt();
|
||||
return false;
|
||||
case CONTAINS:
|
||||
if (left.type() == QVariant::Type::StringList &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
return left.toStringList().contains(right.toString(),
|
||||
Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
if (left.type() == QVariant::Type::Map &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
return left.toMap().contains(right.toString());
|
||||
}
|
||||
|
||||
if (left.canConvert(QMetaType::QString) &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
return left.toString().contains(right.toString(),
|
||||
Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return false;
|
||||
case STARTS_WITH:
|
||||
if (left.type() == QVariant::Type::StringList &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
auto list = left.toStringList();
|
||||
return !list.isEmpty() &&
|
||||
list.first().compare(right.toString(),
|
||||
Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
if (left.canConvert(QMetaType::QString) &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
return left.toString().startsWith(right.toString(),
|
||||
Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
case ENDS_WITH:
|
||||
if (left.type() == QVariant::Type::StringList &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
auto list = left.toStringList();
|
||||
return !list.isEmpty() &&
|
||||
list.last().compare(right.toString(),
|
||||
Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
if (left.canConvert(QMetaType::QString) &&
|
||||
right.canConvert(QMetaType::QString))
|
||||
{
|
||||
return left.toString().endsWith(right.toString(),
|
||||
Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QString BinaryOperation::debug() const
|
||||
{
|
||||
return QString("(%1 %2 %3)")
|
||||
.arg(this->left_->debug(), tokenTypeToInfoString(this->op_),
|
||||
this->right_->debug());
|
||||
}
|
||||
|
||||
QString BinaryOperation::filterString() const
|
||||
{
|
||||
const auto opText = [&]() -> QString {
|
||||
switch (this->op_)
|
||||
{
|
||||
case AND:
|
||||
return "&&";
|
||||
case OR:
|
||||
return "||";
|
||||
case PLUS:
|
||||
return "+";
|
||||
case MINUS:
|
||||
return "-";
|
||||
case MULTIPLY:
|
||||
return "*";
|
||||
case DIVIDE:
|
||||
return "/";
|
||||
case MOD:
|
||||
return "%";
|
||||
case EQ:
|
||||
return "==";
|
||||
case NEQ:
|
||||
return "!=";
|
||||
case LT:
|
||||
return "<";
|
||||
case GT:
|
||||
return ">";
|
||||
case LTE:
|
||||
return "<=";
|
||||
case GTE:
|
||||
return ">=";
|
||||
case CONTAINS:
|
||||
return "contains";
|
||||
case STARTS_WITH:
|
||||
return "startswith";
|
||||
case ENDS_WITH:
|
||||
return "endswith";
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}();
|
||||
|
||||
return QString("(%1) %2 (%3)")
|
||||
.arg(this->left_->filterString())
|
||||
.arg(opText)
|
||||
.arg(this->right_->filterString());
|
||||
}
|
||||
|
||||
// UnaryOperation
|
||||
|
||||
UnaryOperation::UnaryOperation(TokenType op, ExpressionPtr right)
|
||||
: op_(op)
|
||||
, right_(std::move(right))
|
||||
{
|
||||
}
|
||||
|
||||
QVariant UnaryOperation::execute(const ContextMap &context) const
|
||||
{
|
||||
auto right = this->right_->execute(context);
|
||||
switch (this->op_)
|
||||
{
|
||||
case NOT:
|
||||
if (right.canConvert<bool>())
|
||||
return !right.toBool();
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QString UnaryOperation::debug() const
|
||||
{
|
||||
return QString("(%1 %2)").arg(tokenTypeToInfoString(this->op_),
|
||||
this->right_->debug());
|
||||
}
|
||||
|
||||
QString UnaryOperation::filterString() const
|
||||
{
|
||||
const auto opText = [&]() -> QString {
|
||||
switch (this->op_)
|
||||
{
|
||||
case NOT:
|
||||
return "!";
|
||||
default:
|
||||
return QString();
|
||||
}
|
||||
}();
|
||||
|
||||
return QString("%1(%2)").arg(opText).arg(this->right_->filterString());
|
||||
}
|
||||
|
||||
} // namespace filterparser
|
||||
@@ -0,0 +1,125 @@
|
||||
#pragma once
|
||||
|
||||
#include "messages/Message.hpp"
|
||||
|
||||
namespace filterparser {
|
||||
|
||||
using MessagePtr = std::shared_ptr<const chatterino::Message>;
|
||||
using ContextMap = QMap<QString, QVariant>;
|
||||
|
||||
enum TokenType {
|
||||
// control
|
||||
CONTROL_START = 0,
|
||||
AND = 1,
|
||||
OR = 2,
|
||||
LP = 3,
|
||||
RP = 4,
|
||||
CONTROL_END = 9,
|
||||
|
||||
// binary operator
|
||||
BINARY_START = 10,
|
||||
EQ = 11,
|
||||
NEQ = 12,
|
||||
LT = 13,
|
||||
GT = 14,
|
||||
LTE = 15,
|
||||
GTE = 16,
|
||||
CONTAINS = 17,
|
||||
STARTS_WITH = 18,
|
||||
ENDS_WITH = 19,
|
||||
BINARY_END = 49,
|
||||
|
||||
// unary operator
|
||||
UNARY_START = 50,
|
||||
NOT = 51,
|
||||
UNARY_END = 99,
|
||||
|
||||
// math operators
|
||||
MATH_START = 100,
|
||||
PLUS = 101,
|
||||
MINUS = 102,
|
||||
MULTIPLY = 103,
|
||||
DIVIDE = 104,
|
||||
MOD = 105,
|
||||
MATH_END = 149,
|
||||
|
||||
// other types
|
||||
OTHER_START = 150,
|
||||
STRING = 151,
|
||||
INT = 152,
|
||||
IDENTIFIER = 153,
|
||||
|
||||
NONE = 200
|
||||
};
|
||||
|
||||
bool convertVariantTypes(QVariant &a, QVariant &b, int type);
|
||||
QString tokenTypeToInfoString(TokenType type);
|
||||
|
||||
class Expression
|
||||
{
|
||||
public:
|
||||
virtual ~Expression() = default;
|
||||
|
||||
virtual QVariant execute(const ContextMap &) const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual QString debug() const
|
||||
{
|
||||
return "(false)";
|
||||
}
|
||||
|
||||
virtual QString filterString() const
|
||||
{
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
using ExpressionPtr = std::unique_ptr<Expression>;
|
||||
|
||||
class ValueExpression : public Expression
|
||||
{
|
||||
public:
|
||||
ValueExpression(QVariant value, TokenType type);
|
||||
TokenType type();
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
QString debug() const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
QVariant value_;
|
||||
TokenType type_;
|
||||
};
|
||||
|
||||
class BinaryOperation : public Expression
|
||||
{
|
||||
public:
|
||||
BinaryOperation(TokenType op, ExpressionPtr left, ExpressionPtr right);
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
QString debug() const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
TokenType op_;
|
||||
ExpressionPtr left_;
|
||||
ExpressionPtr right_;
|
||||
};
|
||||
|
||||
class UnaryOperation : public Expression
|
||||
{
|
||||
public:
|
||||
UnaryOperation(TokenType op, ExpressionPtr right);
|
||||
|
||||
QVariant execute(const ContextMap &context) const override;
|
||||
QString debug() const override;
|
||||
QString filterString() const override;
|
||||
|
||||
private:
|
||||
TokenType op_;
|
||||
ExpressionPtr right_;
|
||||
};
|
||||
|
||||
} // namespace filterparser
|
||||
Reference in New Issue
Block a user