Skip to content

Commit

Permalink
Implement type checking/validation for filters (#4364)
Browse files Browse the repository at this point in the history
Co-authored-by: pajlada <rasmus.karlsson@pajlada.com>
  • Loading branch information
dnsge and pajlada authored Apr 9, 2023
1 parent c8e1741 commit 34db692
Show file tree
Hide file tree
Showing 30 changed files with 1,449 additions and 629 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Minor: Added support for FrankerFaceZ animated emotes. (#4434)
- Minor: Added a local backup of the Twitch Badges API in case the request fails. (#4463)
- Minor: Added the ability to reply to a message by `Shift + Right Click`ing the username. (#4424)
- Minor: Added better filter validation and error messages. (#4364)
- Minor: Updated the look of the Black Theme to be more in line with the other themes. (#4523)
- Bugfix: Fixed an issue where animated emotes would render on top of zero-width emotes. (#4314)
- Bugfix: Fixed an issue where it was difficult to hover a zero-width emote. (#4314)
Expand Down
26 changes: 20 additions & 6 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,26 @@ set(SOURCE_FILES
controllers/filters/FilterRecord.hpp
controllers/filters/FilterSet.cpp
controllers/filters/FilterSet.hpp
controllers/filters/parser/FilterParser.cpp
controllers/filters/parser/FilterParser.hpp
controllers/filters/parser/Tokenizer.cpp
controllers/filters/parser/Tokenizer.hpp
controllers/filters/parser/Types.cpp
controllers/filters/parser/Types.hpp
controllers/filters/lang/expressions/Expression.cpp
controllers/filters/lang/expressions/Expression.hpp
controllers/filters/lang/expressions/BinaryOperation.cpp
controllers/filters/lang/expressions/BinaryOperation.hpp
controllers/filters/lang/expressions/ListExpression.cpp
controllers/filters/lang/expressions/ListExpression.hpp
controllers/filters/lang/expressions/RegexExpression.cpp
controllers/filters/lang/expressions/RegexExpression.hpp
controllers/filters/lang/expressions/UnaryOperation.hpp
controllers/filters/lang/expressions/UnaryOperation.cpp
controllers/filters/lang/expressions/ValueExpression.cpp
controllers/filters/lang/expressions/ValueExpression.hpp
controllers/filters/lang/Filter.cpp
controllers/filters/lang/Filter.hpp
controllers/filters/lang/FilterParser.cpp
controllers/filters/lang/FilterParser.hpp
controllers/filters/lang/Tokenizer.cpp
controllers/filters/lang/Tokenizer.hpp
controllers/filters/lang/Types.cpp
controllers/filters/lang/Types.hpp

controllers/highlights/BadgeHighlightModel.cpp
controllers/highlights/BadgeHighlightModel.hpp
Expand Down
48 changes: 34 additions & 14 deletions src/controllers/filters/FilterRecord.cpp
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
#include "controllers/filters/FilterRecord.hpp"

#include "controllers/filters/lang/Filter.hpp"

namespace chatterino {

FilterRecord::FilterRecord(const QString &name, const QString &filter)
: name_(name)
, filter_(filter)
, id_(QUuid::createUuid())
, parser_(std::make_unique<filterparser::FilterParser>(filter))
static std::unique_ptr<filters::Filter> buildFilter(const QString &filterText)
{
using namespace filters;
auto result = Filter::fromString(filterText);
if (std::holds_alternative<Filter>(result))
{
auto filter =
std::make_unique<Filter>(std::move(std::get<Filter>(result)));

if (filter->returnType() != Type::Bool)
{
// Only accept Bool results
return nullptr;
}

return filter;
}

return nullptr;
}

FilterRecord::FilterRecord(QString name, QString filter)
: FilterRecord(std::move(name), std::move(filter), QUuid::createUuid())
{
}

FilterRecord::FilterRecord(const QString &name, const QString &filter,
const QUuid &id)
: name_(name)
, filter_(filter)
FilterRecord::FilterRecord(QString name, QString filter, const QUuid &id)
: name_(std::move(name))
, filterText_(std::move(filter))
, id_(id)
, parser_(std::make_unique<filterparser::FilterParser>(filter))
, filter_(buildFilter(this->filterText_))
{
}

Expand All @@ -26,7 +45,7 @@ const QString &FilterRecord::getName() const

const QString &FilterRecord::getFilter() const
{
return this->filter_;
return this->filterText_;
}

const QUuid &FilterRecord::getId() const
Expand All @@ -36,12 +55,13 @@ const QUuid &FilterRecord::getId() const

bool FilterRecord::valid() const
{
return this->parser_->valid();
return this->filter_ != nullptr;
}

bool FilterRecord::filter(const filterparser::ContextMap &context) const
bool FilterRecord::filter(const filters::ContextMap &context) const
{
return this->parser_->execute(context);
assert(this->valid());
return this->filter_->execute(context).toBool();
}

bool FilterRecord::operator==(const FilterRecord &other) const
Expand Down
16 changes: 8 additions & 8 deletions src/controllers/filters/FilterRecord.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#pragma once

#include "controllers/filters/parser/FilterParser.hpp"
#include "controllers/filters/lang/Filter.hpp"
#include "util/RapidjsonHelpers.hpp"
#include "util/RapidJsonSerializeQString.hpp"

Expand All @@ -16,9 +16,9 @@ namespace chatterino {
class FilterRecord
{
public:
FilterRecord(const QString &name, const QString &filter);
FilterRecord(QString name, QString filter);

FilterRecord(const QString &name, const QString &filter, const QUuid &id);
FilterRecord(QString name, QString filter, const QUuid &id);

const QString &getName() const;

Expand All @@ -28,16 +28,16 @@ class FilterRecord

bool valid() const;

bool filter(const filterparser::ContextMap &context) const;
bool filter(const filters::ContextMap &context) const;

bool operator==(const FilterRecord &other) const;

private:
QString name_;
QString filter_;
QUuid id_;
const QString name_;
const QString filterText_;
const QUuid id_;

std::unique_ptr<filterparser::FilterParser> parser_;
const std::unique_ptr<filters::Filter> filter_;
};

using FilterRecordPtr = std::shared_ptr<FilterRecord>;
Expand Down
3 changes: 1 addition & 2 deletions src/controllers/filters/FilterSet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ bool FilterSet::filter(const MessagePtr &m, ChannelPtr channel) const
if (this->filters_.size() == 0)
return true;

filterparser::ContextMap context =
filterparser::buildContextMap(m, channel.get());
filters::ContextMap context = filters::buildContextMap(m, channel.get());
for (const auto &f : this->filters_.values())
{
if (!f->valid() || !f->filter(context))
Expand Down
161 changes: 161 additions & 0 deletions src/controllers/filters/lang/Filter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#include "controllers/filters/lang/Filter.hpp"

#include "Application.hpp"
#include "common/Channel.hpp"
#include "controllers/filters/lang/FilterParser.hpp"
#include "messages/Message.hpp"
#include "providers/twitch/TwitchBadge.hpp"
#include "providers/twitch/TwitchChannel.hpp"
#include "providers/twitch/TwitchIrcServer.hpp"

namespace chatterino::filters {

ContextMap buildContextMap(const MessagePtr &m, chatterino::Channel *channel)
{
auto watchingChannel = chatterino::getApp()->twitch->watchingChannel.get();

/*
* Looking to add a new identifier to filters? Here's what to do:
* 1. Update validIdentifiersMap in Tokenizer.hpp
* 2. Add the identifier to the list below
* 3. Add the type of the identifier to MESSAGE_TYPING_CONTEXT in Filter.hpp
* 4. Add the value for the identifier to the ContextMap returned by this function
*
* List of 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.reward_message
* flags.first_message
* flags.elevated_message
* flags.cheer_message
* flags.whisper
* flags.reply
* flags.automod
*
* 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 = false;
int subLength = 0;
for (const auto &subBadge : {"subscriber", "founder"})
{
if (!badges.contains(subBadge))
{
continue;
}
subscribed = true;
if (m->badgeInfos.find(subBadge) != m->badgeInfos.end())
{
subLength = m->badgeInfos.at(subBadge).toInt();
}
}
ContextMap vars = {
{"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.reward_message",
m->flags.has(MessageFlag::RedeemedChannelPointReward)},
{"flags.first_message", m->flags.has(MessageFlag::FirstMessage)},
{"flags.elevated_message", m->flags.has(MessageFlag::ElevatedMessage)},
{"flags.cheer_message", m->flags.has(MessageFlag::CheerMessage)},
{"flags.whisper", m->flags.has(MessageFlag::Whisper)},
{"flags.reply", m->flags.has(MessageFlag::ReplyMessage)},
{"flags.automod", m->flags.has(MessageFlag::AutoMod)},

{"message.content", m->messageText},
{"message.length", m->messageText.length()},
};
{
auto *tc = dynamic_cast<TwitchChannel *>(channel);
if (channel && !channel->isEmpty() && tc)
{
vars["channel.live"] = tc->isLive();
}
else
{
vars["channel.live"] = false;
}
}
return vars;
}

FilterResult Filter::fromString(const QString &str)
{
FilterParser parser(str);

if (parser.valid())
{
auto exp = parser.release();
auto typ = parser.returnType();
return Filter(std::move(exp), typ);
}

return FilterError{parser.errors().join("\n")};
}

Filter::Filter(ExpressionPtr expression, Type returnType)
: expression_(std::move(expression))
, returnType_(returnType)
{
}

Type Filter::returnType() const
{
return this->returnType_;
}

QVariant Filter::execute(const ContextMap &context) const
{
return this->expression_->execute(context);
}

QString Filter::filterString() const
{
return this->expression_->filterString();
}

QString Filter::debugString(const TypingContext &context) const
{
return this->expression_->debug(context);
}

} // namespace chatterino::filters
Loading

0 comments on commit 34db692

Please sign in to comment.