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:
dnsge
2020-10-18 15:16:56 +02:00
committed by Rasmus Karlsson
parent 812cbdf4f9
commit 4199a01b96
41 changed files with 2189 additions and 43 deletions
+41
View File
@@ -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
+26
View File
@@ -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
+123
View File
@@ -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
+85
View File
@@ -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
+363
View File
@@ -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
+125
View File
@@ -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