Skip to content
Snippets Groups Projects
Verified Commit 453a7f37 authored by Nicolas Werner's avatar Nicolas Werner
Browse files

Support event_property_is and event_property_contains rules

parent d558041f
No related branches found
No related tags found
No related merge requests found
Pipeline #5623 failed
......@@ -42,6 +42,9 @@ struct PushCondition
//! with no special glob characters should be treated as having asterisks prepended and
//! appended when testing the condition.
std::string pattern;
//! Required for event_property_is and event_property_contains conditions. A non compound json
//! value.
std::optional<std::variant<std::string, std::int64_t, bool, std::nullptr_t>> value;
//! Required for room_member_count conditions. A decimal integer optionally prefixed by one
//! of, ==, <, >, >= or <=. A prefix of < matches rooms where the member count is strictly
//! less than the given number and so forth. If no prefix is present, this parameter
......
......@@ -8,15 +8,41 @@
#include "mtx/events/collections.hpp"
#include "mtx/log.hpp"
using non_compound_json_value = std::variant<std::string, std::int64_t, bool, std::nullptr_t>;
using push_json_value = std::
variant<std::string, std::int64_t, bool, std::nullptr_t, std::vector<non_compound_json_value>>;
namespace {
struct RelatedEvents
{
std::vector<std::unordered_map<std::string, std::string>>
fallbacks; //!< fallback related events
std::vector<std::unordered_map<std::string, std::string>> events; //!< related events
std::vector<std::unordered_map<std::string, push_json_value>>
fallbacks; //!< fallback related events
std::vector<std::unordered_map<std::string, push_json_value>> events; //!< related events
};
}
static std::optional<non_compound_json_value>
to_non_compound_json_value(const nlohmann::json &val)
{
switch (val.type()) {
case nlohmann::json::value_t::string: {
return val.get<std::string>();
}
case nlohmann::json::value_t::null: {
return nullptr;
}
case nlohmann::json::value_t::boolean: {
return val.get<bool>();
}
case nlohmann::json::value_t::number_integer:
case nlohmann::json::value_t::number_unsigned: {
return val.get<std::int64_t>();
}
default:
return std::nullopt;
}
}
namespace mtx {
namespace pushrules {
......@@ -32,6 +58,10 @@ to_json(nlohmann::json &obj, const PushCondition &condition)
obj["is"] = condition.is;
if (condition.rel_type != mtx::common::RelationType::Unsupported)
obj["rel_type"] = condition.rel_type;
if (condition.value) {
std::visit([&obj](const auto &e) { obj["value"] = e; }, *condition.value);
}
}
void
......@@ -43,6 +73,10 @@ from_json(const nlohmann::json &obj, PushCondition &condition)
condition.is = obj.value("is", "");
condition.rel_type = obj.value("rel_type", mtx::common::RelationType::Unsupported);
condition.include_fallback = obj.value("include_fallback", false);
if (obj.contains("value"))
if (auto val = to_non_compound_json_value(obj.at("pattern")))
condition.value = *val;
}
namespace actions {
......@@ -196,15 +230,17 @@ struct PushRuleEvaluator::OptimizedRules
std::unique_ptr<re2::RE2> pattern; //!< the pattern
std::string field; //!< the field to match with pattern
bool matches(const std::unordered_map<std::string, std::string> &ev) const
[[nodiscard]] bool matches(
const std::unordered_map<std::string, push_json_value> &ev) const
{
if (auto it = ev.find(field); it != ev.end()) {
if (pattern) {
if (pattern && std::holds_alternative<std::string>(it->second)) {
if (field == "content.body") {
if (!re2::RE2::PartialMatch(it->second, *pattern))
if (!re2::RE2::PartialMatch(std::get<std::string>(it->second),
*pattern))
return false;
} else {
if (!re2::RE2::FullMatch(it->second, *pattern))
if (!re2::RE2::FullMatch(std::get<std::string>(it->second), *pattern))
return false;
}
}
......@@ -218,6 +254,45 @@ struct PushRuleEvaluator::OptimizedRules
// TODO(Nico): Sort by field for faster matching?
std::vector<PatternCondition> patterns; //!< conditions that match on a field
//! a event_property_is condition to match
struct IsCondition
{
non_compound_json_value value; //!< the pattern
std::string field; //!< the field to match with pattern
[[nodiscard]] bool matches(
const std::unordered_map<std::string, push_json_value> &ev) const
{
if (auto it = ev.find(field); it != ev.end()) {
return std::visit(
[it](const auto &e) { return it->second == push_json_value(e); }, value);
}
return false;
}
};
std::vector<IsCondition> is; //!< conditions that match on a field being an exact value
//!
//! a event_property_contains condition to match
struct ContainsCondition
{
non_compound_json_value value; //!< the pattern
std::string field; //!< the field to match with pattern
[[nodiscard]] bool matches(
const std::unordered_map<std::string, push_json_value> &ev) const
{
if (auto it = ev.find(field); it != ev.end()) {
if (auto arr = std::get_if<std::vector<non_compound_json_value>>(&it->second)) {
return std::find(arr->begin(), arr->end(), value) != arr->end();
}
}
return false;
}
};
std::vector<ContainsCondition> contains; //!< conditions that match on arrays
//! a pattern condition to match on a related event
struct RelatedEventCondition
{
......@@ -255,7 +330,7 @@ struct PushRuleEvaluator::OptimizedRules
std::vector<actions::Action> actions; //< the actions to apply on match
[[nodiscard]] bool matches(
const std::unordered_map<std::string, std::string> &ev,
const std::unordered_map<std::string, push_json_value> &ev,
const PushRuleEvaluator::RoomContext &ctx,
const std::map<mtx::common::RelationType, RelatedEvents> &relatedEventsFlat) const
{
......@@ -284,7 +359,8 @@ struct PushRuleEvaluator::OptimizedRules
if (sender_ == ev.end())
return false;
auto sender_level = ctx.power_levels.user_level(sender_->second);
auto sender_level =
ctx.power_levels.user_level(std::get<std::string>(sender_->second));
for (const auto &n : notification_levels) {
if (sender_level < ctx.power_levels.notification_level(n))
......@@ -296,6 +372,14 @@ struct PushRuleEvaluator::OptimizedRules
if (!cond.matches(ev))
return false;
}
for (const auto &cond : is) {
if (!cond.matches(ev))
return false;
}
for (const auto &cond : contains) {
if (!cond.matches(ev))
return false;
}
for (const auto &cond : related_event_patterns) {
bool matched = false;
......@@ -327,12 +411,13 @@ struct PushRuleEvaluator::OptimizedRules
if (ctx.user_display_name.empty())
return false;
if (auto it = ev.find("content.body"); it != ev.end()) {
if (auto it = ev.find("content.body");
it != ev.end() && std::holds_alternative<std::string>(it->second)) {
re2::RE2::Options opts;
opts.set_case_sensitive(false);
if (!re2::RE2::PartialMatch(
it->second,
std::get<std::string>(it->second),
re2::RE2("(\\W|^)" + re2::RE2::QuoteMeta(ctx.user_display_name) +
"(\\W|$)",
opts)))
......@@ -387,6 +472,16 @@ PushRuleEvaluator::PushRuleEvaluator(const Ruleset &rules_)
c.pattern = construct_re_from_pattern(cond.pattern, cond.key);
if (c.pattern)
rule.patterns.push_back(std::move(c));
} else if (cond.kind == "event_property_is" && cond.value) {
OptimizedRules::OptimizedRule::IsCondition c;
c.field = cond.key;
c.value = cond.value.value();
rule.is.push_back(std::move(c));
} else if (cond.kind == "event_property_contains" && cond.value) {
OptimizedRules::OptimizedRule::ContainsCondition c;
c.field = cond.key;
c.value = cond.value.value();
rule.contains.push_back(std::move(c));
} else if (cond.kind == "im.nheko.msc3664.related_event_match") {
OptimizedRules::OptimizedRule::RelatedEventCondition c;
......@@ -516,13 +611,23 @@ PushRuleEvaluator::PushRuleEvaluator(const Ruleset &rules_)
static void
flatten_impl(const nlohmann::json &value,
std::unordered_map<std::string, std::string> &result,
std::unordered_map<std::string, push_json_value> &result,
const std::string &current_path,
int current_depth)
{
if (current_depth > 100)
return;
auto escape_key = [](std::string input) {
for (size_t i = 0; i < input.size(); i++) {
if (input[i] == '.') {
input.insert(i, 1, '\\');
i++;
}
}
return input;
};
switch (value.type()) {
case nlohmann::json::value_t::object: {
// iterate object and use keys as reference string
......@@ -530,24 +635,61 @@ flatten_impl(const nlohmann::json &value,
if (!current_path.empty())
prefix = current_path + ".";
for (const auto &element : value.items()) {
flatten_impl(element.value(), result, prefix + element.key(), current_depth + 1);
flatten_impl(
element.value(), result, prefix + escape_key(element.key()), current_depth + 1);
}
break;
}
case nlohmann::json::value_t::array: {
std::vector<non_compound_json_value> arr;
for (const auto &val : value) {
switch (val.type()) {
case nlohmann::json::value_t::string: {
arr.emplace_back(val.get<std::string>());
break;
}
case nlohmann::json::value_t::null: {
arr.emplace_back(nullptr);
break;
}
case nlohmann::json::value_t::boolean: {
arr.emplace_back(val.get<bool>());
break;
}
case nlohmann::json::value_t::number_integer:
case nlohmann::json::value_t::number_unsigned: {
arr.emplace_back(val.get<std::int64_t>());
break;
}
default:
break;
}
}
result[current_path] = arr;
break;
}
case nlohmann::json::value_t::string: {
// add primitive value with its reference string
result[current_path] = value.get<std::string>();
break;
}
// currently we only match strings
case nlohmann::json::value_t::array:
case nlohmann::json::value_t::null:
case nlohmann::json::value_t::boolean:
case nlohmann::json::value_t::null: {
result[current_path] = nullptr;
break;
}
case nlohmann::json::value_t::boolean: {
result[current_path] = value.get<bool>();
break;
}
case nlohmann::json::value_t::number_integer:
case nlohmann::json::value_t::number_unsigned:
case nlohmann::json::value_t::number_float:
case nlohmann::json::value_t::number_unsigned: {
result[current_path] = value.get<std::int64_t>();
break;
}
case nlohmann::json::value_t::number_float: // matrix events can't have floats
case nlohmann::json::value_t::binary:
case nlohmann::json::value_t::discarded:
default:
......@@ -555,10 +697,10 @@ flatten_impl(const nlohmann::json &value,
}
}
static std::unordered_map<std::string, std::string>
static std::unordered_map<std::string, push_json_value>
flatten_event(const nlohmann::json &j)
{
std::unordered_map<std::string, std::string> flat;
std::unordered_map<std::string, push_json_value> flat;
flatten_impl(j, flat, "", 0);
return flat;
}
......
......@@ -424,6 +424,7 @@ TEST(Pushrules, EventMatches)
.kind = "event_match",
.key = "content.body",
.pattern = "honk",
.value = std::nullopt,
.is = "",
});
......@@ -513,6 +514,7 @@ TEST(Pushrules, DisplaynameMatches)
.kind = "contains_display_name",
.key = "",
.pattern = "",
.value = std::nullopt,
.is = "",
});
......@@ -586,6 +588,7 @@ TEST(Pushrules, PowerLevelMatches)
.kind = "sender_notification_permission",
.key = "room",
.pattern = "",
.value = std::nullopt,
.is = "",
});
......@@ -642,6 +645,7 @@ TEST(Pushrules, MemberCountMatches)
.kind = "room_member_count",
.key = "",
.pattern = "",
.value = std::nullopt,
.is = is,
},
};
......@@ -667,6 +671,128 @@ TEST(Pushrules, MemberCountMatches)
testEval("<100", true, false, false);
}
TEST(Pushrules, EventPropertyIsMatches)
{
mtx::pushrules::PushRule event_match_rule;
event_match_rule.actions = {
mtx::pushrules::actions::notify{},
mtx::pushrules::actions::set_tweak_highlight{},
};
event_match_rule.conditions.push_back(mtx::pushrules::PushCondition{
.kind = "event_property_is",
.key = "content.body",
.pattern = "",
.value = "honk",
.is = "",
});
mtx::events::RoomEvent<mtx::events::msg::Text> textEv{};
textEv.content.body = "honk";
textEv.room_id = "!abc:def.ghi";
textEv.event_id = "$abc1234567890:def.ghi";
textEv.sender = "@me:def.ghi";
auto testEval = [actions = event_match_rule.actions,
&textEv](const mtx::pushrules::PushRuleEvaluator &evaluator) {
mtx::pushrules::PushRuleEvaluator::RoomContext ctx{};
EXPECT_EQ(evaluator.evaluate({textEv}, ctx, {}), actions);
auto textEvNo = textEv;
textEvNo.content.body = "hon";
EXPECT_TRUE(evaluator.evaluate({textEvNo}, ctx, {}).empty());
auto textEvNo2 = textEv;
textEvNo2.content.body = "honkb";
EXPECT_TRUE(evaluator.evaluate({textEvNo2}, ctx, {}).empty());
auto textEvWordBoundaries = textEv;
textEvWordBoundaries.content.body = "@honk:";
EXPECT_TRUE(evaluator.evaluate({textEvWordBoundaries}, ctx, {}).empty());
};
mtx::pushrules::Ruleset override_ruleset;
override_ruleset.override_.push_back(event_match_rule);
mtx::pushrules::PushRuleEvaluator over_evaluator{override_ruleset};
testEval(over_evaluator);
mtx::pushrules::Ruleset underride_ruleset;
underride_ruleset.underride.push_back(event_match_rule);
mtx::pushrules::PushRuleEvaluator under_evaluator{underride_ruleset};
testEval(under_evaluator);
mtx::pushrules::Ruleset room_ruleset;
auto room_rule = event_match_rule;
room_rule.rule_id = "!abc:def.ghi";
room_ruleset.room.push_back(room_rule);
mtx::pushrules::PushRuleEvaluator room_evaluator{room_ruleset};
EXPECT_EQ(room_evaluator.evaluate({textEv}, {}, {}), room_rule.actions);
mtx::pushrules::Ruleset sender_ruleset;
auto sender_rule = event_match_rule;
sender_rule.rule_id = "@me:def.ghi";
sender_ruleset.sender.push_back(sender_rule);
mtx::pushrules::PushRuleEvaluator sender_evaluator{sender_ruleset};
EXPECT_EQ(sender_evaluator.evaluate({textEv}, {}, {}), sender_rule.actions);
mtx::pushrules::PushRule event_match_5_rule;
event_match_rule.actions = {
mtx::pushrules::actions::notify{},
mtx::pushrules::actions::set_tweak_highlight{},
};
event_match_rule.conditions.push_back(mtx::pushrules::PushCondition{
.kind = "event_property_is",
.key = "content.info.size",
.pattern = "",
.value = 5,
.is = "",
});
mtx::pushrules::Ruleset match5_ruleset;
match5_ruleset.override_.push_back(event_match_rule);
mtx::pushrules::PushRuleEvaluator over_evaluator5{match5_ruleset};
mtx::events::RoomEvent<mtx::events::msg::Image> imageEv{};
imageEv.content.info.size = 5;
mtx::pushrules::PushRuleEvaluator::RoomContext ctx{};
EXPECT_EQ(over_evaluator5.evaluate({imageEv}, ctx, {}), event_match_5_rule.actions);
imageEv.content.info.size = 6;
EXPECT_TRUE(over_evaluator5.evaluate({imageEv}, ctx, {}).empty());
}
TEST(Pushrules, EventPropertyContainsMatches)
{
mtx::pushrules::PushRule event_match_rule;
event_match_rule.actions = {
mtx::pushrules::actions::notify{},
mtx::pushrules::actions::set_tweak_highlight{},
};
event_match_rule.conditions.push_back(mtx::pushrules::PushCondition{
.kind = "event_property_contains",
.key = "content.via",
.pattern = "",
.value = "honk",
.is = "",
});
mtx::events::StateEvent<mtx::events::state::space::Child> childEv{};
childEv.content.via = {"honk"};
childEv.room_id = "!abc:def.ghi";
childEv.event_id = "$abc1234567890:def.ghi";
childEv.sender = "@me:def.ghi";
mtx::pushrules::Ruleset override_ruleset;
override_ruleset.override_.push_back(event_match_rule);
mtx::pushrules::PushRuleEvaluator evaluator{override_ruleset};
mtx::pushrules::PushRuleEvaluator::RoomContext ctx{};
EXPECT_EQ(evaluator.evaluate({childEv}, ctx, {}), event_match_rule.actions);
childEv.content.via = {"abc", "honk", ""};
EXPECT_EQ(evaluator.evaluate({childEv}, ctx, {}), event_match_rule.actions);
childEv.content.via = {};
EXPECT_TRUE(evaluator.evaluate({childEv}, ctx, {}).empty());
childEv.content.via = {"not honk"};
EXPECT_TRUE(evaluator.evaluate({childEv}, ctx, {}).empty());
}
TEST(Pushrules, ContentOverRoomRulesMatches)
{
json raw_rule = R"(
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment