diff --git a/include/mtx/pushrules.hpp b/include/mtx/pushrules.hpp index bcc20de8ceb85bee36e6a35c0d724564bfbfa0dd..c891c1e81221ce598ba5b9961124d3c54ceba03b 100644 --- a/include/mtx/pushrules.hpp +++ b/include/mtx/pushrules.hpp @@ -14,6 +14,7 @@ #include <variant> #include <vector> +#include "mtx/events/common.hpp" #include "mtx/events/power_levels.hpp" namespace mtx { @@ -47,6 +48,12 @@ struct PushCondition //! defaults to ==. std::string is; + //! The relation type to match on. Only valid for `im.nheko.msc3664.related_event_match` + //! conditions. + mtx::common::RelationType rel_type = mtx::common::RelationType::Unsupported; + //! Wether to match fallback relations or not. + bool include_fallback = false; + friend void to_json(nlohmann::json &obj, const PushCondition &condition); friend void from_json(const nlohmann::json &obj, PushCondition &condition); }; @@ -200,10 +207,14 @@ public: //! Evaluate the pushrules for @event . /// /// You need to have the room_id set for the event. + /// `relatedEvents` is a mapping of rel_type to event. Pass all the events that are related to + /// by this event here. /// \returns the actions to apply. [[nodiscard]] std::vector<actions::Action> evaluate( const mtx::events::collections::TimelineEvent &event, - const RoomContext &ctx) const; + const RoomContext &ctx, + const std::vector<std::pair<mtx::common::Relation, mtx::events::collections::TimelineEvent>> + &relatedEvents) const; private: struct OptimizedRules; diff --git a/lib/structs/events/common.cpp b/lib/structs/events/common.cpp index 6983b239e8e9b325f8a38cce37694a1ec33ece34..779972129180617c8e4af0a61345aea7c47c2a34 100644 --- a/lib/structs/events/common.cpp +++ b/lib/structs/events/common.cpp @@ -204,7 +204,8 @@ from_json(const json &obj, RelationType &type) type = RelationType::Reference; else if (obj.get<std::string>() == "m.replace") type = RelationType::Replace; - else if (obj.get<std::string>() == "im.nheko.relations.v1.in_reply_to") + else if (obj.get<std::string>() == "im.nheko.relations.v1.in_reply_to" || + obj.get<std::string>() == "m.in_reply_to") type = RelationType::InReplyTo; else if (obj.get<std::string>() == "m.thread") type = RelationType::Thread; diff --git a/lib/structs/pushrules.cpp b/lib/structs/pushrules.cpp index 57d1019bcbcf33c373ed5d78f80102ab7ed3b18f..77e8b4fd0a15cd933919dd3af6975f43a2409d84 100644 --- a/lib/structs/pushrules.cpp +++ b/lib/structs/pushrules.cpp @@ -8,6 +8,15 @@ #include "mtx/events/collections.hpp" #include "mtx/log.hpp" +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 +}; +} + namespace mtx { namespace pushrules { @@ -21,15 +30,19 @@ to_json(nlohmann::json &obj, const PushCondition &condition) obj["pattern"] = condition.pattern; if (!condition.is.empty()) obj["is"] = condition.is; + if (condition.rel_type != mtx::common::RelationType::Unsupported) + obj["rel_type"] = condition.rel_type; } void from_json(const nlohmann::json &obj, PushCondition &condition) { - condition.kind = obj["kind"].get<std::string>(); - condition.key = obj.value("key", ""); - condition.pattern = obj.value("pattern", ""); - condition.is = obj.value("is", ""); + condition.kind = obj["kind"].get<std::string>(); + condition.key = obj.value("key", ""); + condition.pattern = obj.value("pattern", ""); + condition.is = obj.value("is", ""); + condition.rel_type = obj.value("rel_type", mtx::common::RelationType::Unsupported); + condition.include_fallback = obj.value("include_fallback", false); } namespace actions { @@ -180,11 +193,40 @@ struct PushRuleEvaluator::OptimizedRules //! a pattern condition to match struct PatternCondition { - std::unique_ptr<re2::RE2> pattern; //< the pattern - std::string field; //< the field to match with pattern + 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 + { + if (auto it = ev.find(field); it != ev.end()) { + if (pattern) { + if (field == "content.body") { + if (!re2::RE2::PartialMatch(it->second, *pattern)) + return false; + } else { + if (!re2::RE2::FullMatch(it->second, *pattern)) + return false; + } + } + } else { + return false; + } + + return true; + } }; // TODO(Nico): Sort by field for faster matching? - std::vector<PatternCondition> patterns; //< conditions that match on a field + std::vector<PatternCondition> patterns; //!< conditions that match on a field + + //! a pattern condition to match on a related event + struct RelatedEventCondition + { + PatternCondition ev_match; + mtx::common::RelationType rel_type = mtx::common::RelationType::Unsupported; + bool include_fallbacks = false; + }; + std::vector<RelatedEventCondition> + related_event_patterns; //!< conditions that match on fields of the related event. //! a member count condition struct MemberCountCondition @@ -212,8 +254,10 @@ 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 PushRuleEvaluator::RoomContext &ctx) const + [[nodiscard]] bool matches( + const std::unordered_map<std::string, std::string> &ev, + const PushRuleEvaluator::RoomContext &ctx, + const std::map<mtx::common::RelationType, RelatedEvents> &relatedEventsFlat) const { for (const auto &cond : membercounts) { if (![&cond, &ctx] { @@ -249,19 +293,34 @@ struct PushRuleEvaluator::OptimizedRules } for (const auto &cond : patterns) { - if (auto it = ev.find(cond.field); it != ev.end()) { - if (cond.pattern) { - if (cond.field == "content.body") { - if (!re2::RE2::PartialMatch(it->second, *cond.pattern)) - return false; - } else { - if (!re2::RE2::FullMatch(it->second, *cond.pattern)) - return false; + if (!cond.matches(ev)) + return false; + } + + for (const auto &cond : related_event_patterns) { + bool matched = false; + for (const auto &[rel_type, rel_ev] : relatedEventsFlat) { + if (cond.rel_type == rel_type) { + for (const auto &e : rel_ev.events) { + if (cond.ev_match.field.empty() || !cond.ev_match.pattern || + cond.ev_match.matches(e)) { + matched = true; + break; + } + } + if (cond.include_fallbacks) { + for (const auto &e : rel_ev.fallbacks) { + if (cond.ev_match.field.empty() || !cond.ev_match.pattern || + cond.ev_match.matches(e)) { + matched = true; + break; + } + } } } - } else { - return false; } + if (!matched) + return false; } if (check_displayname) { @@ -325,6 +384,23 @@ 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 == "im.nheko.msc3664.related_event_match") { + OptimizedRules::OptimizedRule::RelatedEventCondition c; + + if (cond.rel_type != mtx::common::RelationType::Unsupported) { + c.rel_type = cond.rel_type; + c.include_fallbacks = cond.include_fallback; + + if (!cond.key.empty() && !cond.pattern.empty()) { + c.ev_match.field = cond.key; + c.ev_match.pattern = construct_re_from_pattern(cond.pattern, cond.key); + } + rule.related_event_patterns.push_back(std::move(c)); + } else { + mtx::utils::log::log()->info( + "Skipping rel_event_match rule with unknown rel_type."); + return false; + } } else if (cond.kind == "contains_display_name") { rule.check_displayname = true; } else if (cond.kind == "room_member_count") { @@ -479,19 +555,33 @@ flatten_event(const nlohmann::json &j) } std::vector<actions::Action> -PushRuleEvaluator::evaluate(const mtx::events::collections::TimelineEvent &event, - const RoomContext &ctx) const +PushRuleEvaluator::evaluate( + const mtx::events::collections::TimelineEvent &event, + const RoomContext &ctx, + const std::vector<std::pair<mtx::common::Relation, mtx::events::collections::TimelineEvent>> + &relatedEvents) const { auto event_json = nlohmann::json(event); auto flat_event = flatten_event(event_json); + std::map<mtx::common::RelationType, RelatedEvents> relatedEventsFlat; + for (const auto &[rel, ev] : relatedEvents) { + if (rel.rel_type != mtx::common::RelationType::Unsupported) { + if (rel.is_fallback) + relatedEventsFlat[rel.rel_type].fallbacks.push_back( + flatten_event(nlohmann::json(ev))); + else + relatedEventsFlat[rel.rel_type].events.push_back(flatten_event(nlohmann::json(ev))); + } + } + for (const auto &rule : rules->override_) { - if (rule.matches(flat_event, ctx)) + if (rule.matches(flat_event, ctx, relatedEventsFlat)) return rule.actions; } for (const auto &rule : rules->content) { - if (rule.matches(flat_event, ctx)) + if (rule.matches(flat_event, ctx, relatedEventsFlat)) return rule.actions; } @@ -508,7 +598,7 @@ PushRuleEvaluator::evaluate(const mtx::events::collections::TimelineEvent &event } for (const auto &rule : rules->underride) { - if (rule.matches(flat_event, ctx)) + if (rule.matches(flat_event, ctx, relatedEventsFlat)) return rule.actions; } return {}; diff --git a/tests/pushrules.cpp b/tests/pushrules.cpp index a4f1265e26a27b6908d6075487947ffbdaa33bd7..07b340abc7f50f7383c87f2054adbeeff6b3564d 100644 --- a/tests/pushrules.cpp +++ b/tests/pushrules.cpp @@ -436,37 +436,37 @@ TEST(Pushrules, EventMatches) 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); + EXPECT_EQ(evaluator.evaluate({textEv}, ctx, {}), actions); auto textEvEnd = textEv; textEvEnd.content.body = "abc honk"; - EXPECT_EQ(evaluator.evaluate({textEvEnd}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvEnd}, ctx, {}), actions); auto textEvStart = textEv; textEvStart.content.body = "honk abc"; - EXPECT_EQ(evaluator.evaluate({textEvStart}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvStart}, ctx, {}), actions); auto textEvNL = textEv; textEvNL.content.body = "abc\nhonk\nabc"; - EXPECT_EQ(evaluator.evaluate({textEvNL}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvNL}, ctx, {}), actions); auto textEvFull = textEv; textEvFull.content.body = "honk"; - EXPECT_EQ(evaluator.evaluate({textEvFull}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvFull}, ctx, {}), actions); auto textEvCase = textEv; textEvCase.content.body = "HoNk"; - EXPECT_EQ(evaluator.evaluate({textEvCase}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvCase}, ctx, {}), actions); auto textEvNo = textEv; textEvNo.content.body = "HoN"; - EXPECT_TRUE(evaluator.evaluate({textEvNo}, ctx).empty()); + EXPECT_TRUE(evaluator.evaluate({textEvNo}, ctx, {}).empty()); auto textEvNo2 = textEv; textEvNo2.content.body = "honkb"; - EXPECT_TRUE(evaluator.evaluate({textEvNo2}, ctx).empty()); + EXPECT_TRUE(evaluator.evaluate({textEvNo2}, ctx, {}).empty()); auto textEvWordBoundaries = textEv; textEvWordBoundaries.content.body = "@honk:"; - EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries}, ctx, {}), actions); // It is what the spec says ¯\_(ツ)_/¯ auto textEvWordBoundaries2 = textEv; textEvWordBoundaries2.content.body = "ähonkü"; - EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries2}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries2}, ctx, {}), actions); }; mtx::pushrules::Ruleset override_ruleset; @@ -484,14 +484,14 @@ TEST(Pushrules, EventMatches) 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); + 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); + EXPECT_EQ(sender_evaluator.evaluate({textEv}, {}, {}), sender_rule.actions); mtx::pushrules::Ruleset content_ruleset; mtx::pushrules::PushRule content_match_rule; @@ -527,37 +527,37 @@ TEST(Pushrules, DisplaynameMatches) mtx::pushrules::PushRuleEvaluator::RoomContext ctx{}; ctx.user_display_name = "honk"; - EXPECT_EQ(evaluator.evaluate({textEv}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEv}, ctx, {}), actions); auto textEvEnd = textEv; textEvEnd.content.body = "abc honk"; - EXPECT_EQ(evaluator.evaluate({textEvEnd}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvEnd}, ctx, {}), actions); auto textEvStart = textEv; textEvStart.content.body = "honk abc"; - EXPECT_EQ(evaluator.evaluate({textEvStart}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvStart}, ctx, {}), actions); auto textEvNL = textEv; textEvNL.content.body = "abc\nhonk\nabc"; - EXPECT_EQ(evaluator.evaluate({textEvNL}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvNL}, ctx, {}), actions); auto textEvFull = textEv; textEvFull.content.body = "honk"; - EXPECT_EQ(evaluator.evaluate({textEvFull}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvFull}, ctx, {}), actions); auto textEvCase = textEv; textEvCase.content.body = "HoNk"; - EXPECT_EQ(evaluator.evaluate({textEvCase}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvCase}, ctx, {}), actions); auto textEvNo = textEv; textEvNo.content.body = "HoN"; - EXPECT_TRUE(evaluator.evaluate({textEvNo}, ctx).empty()); + EXPECT_TRUE(evaluator.evaluate({textEvNo}, ctx, {}).empty()); auto textEvNo2 = textEv; textEvNo2.content.body = "honkb"; - EXPECT_TRUE(evaluator.evaluate({textEvNo2}, ctx).empty()); + EXPECT_TRUE(evaluator.evaluate({textEvNo2}, ctx, {}).empty()); auto textEvWordBoundaries = textEv; textEvWordBoundaries.content.body = "@honk:"; - EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries}, ctx, {}), actions); // It is what the spec says ¯\_(ツ)_/¯ auto textEvWordBoundaries2 = textEv; textEvWordBoundaries2.content.body = "ähonkü"; - EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries2}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEvWordBoundaries2}, ctx, {}), actions); }; mtx::pushrules::Ruleset override_ruleset; @@ -602,10 +602,10 @@ TEST(Pushrules, PowerLevelMatches) .power_levels = pls, }; - EXPECT_EQ(evaluator.evaluate({textEv}, ctx), actions); + EXPECT_EQ(evaluator.evaluate({textEv}, ctx, {}), actions); ctx.power_levels.users["@me:def.ghi"] = 0; - EXPECT_TRUE(evaluator.evaluate({textEv}, ctx).empty()); + EXPECT_TRUE(evaluator.evaluate({textEv}, ctx, {}).empty()); }; mtx::pushrules::Ruleset override_ruleset; @@ -648,11 +648,11 @@ TEST(Pushrules, MemberCountMatches) mtx::pushrules::PushRuleEvaluator::RoomContext ctx{}; ctx.member_count = 99; - EXPECT_EQ(!evaluator.evaluate({textEv}, ctx).empty(), lt); + EXPECT_EQ(!evaluator.evaluate({textEv}, ctx, {}).empty(), lt); ctx.member_count = 100; - EXPECT_EQ(!evaluator.evaluate({textEv}, ctx).empty(), eq); + EXPECT_EQ(!evaluator.evaluate({textEv}, ctx, {}).empty(), eq); ctx.member_count = 101; - EXPECT_EQ(!evaluator.evaluate({textEv}, ctx).empty(), gt); + EXPECT_EQ(!evaluator.evaluate({textEv}, ctx, {}).empty(), gt); }; testEval("100", false, true, false); @@ -1021,7 +1021,7 @@ TEST(Pushrules, ContentOverRoomRulesMatches) mtx::pushrules::PushRuleEvaluator evaluator{ruleset.global}; mtx::pushrules::PushRuleEvaluator::RoomContext ctx{}; - auto actions = evaluator.evaluate({text}, ctx); + auto actions = evaluator.evaluate({text}, ctx, {}); auto notifies = [](const std::vector<mtx::pushrules::actions::Action> &a) { for (const auto &action : a) { @@ -1181,7 +1181,8 @@ TEST(Pushrules, ReactionDoesNotMatch) { "key": "sender", "kind": "im.nheko.msc3664.related_event_match", - "pattern": "@deepbluev7:neko.dev" + "pattern": "@deepbluev7:neko.dev", + "rel_type": "m.in_reply_to" } ], "default": true, @@ -1471,7 +1472,7 @@ TEST(Pushrules, ReactionDoesNotMatch) mtx::pushrules::PushRuleEvaluator evaluator{ruleset.global}; mtx::pushrules::PushRuleEvaluator::RoomContext ctx{}; - auto actions = evaluator.evaluate({text}, ctx); + auto actions = evaluator.evaluate({text}, ctx, {}); auto notifies = [](const std::vector<mtx::pushrules::actions::Action> &a) { for (const auto &action : a) { @@ -1871,7 +1872,7 @@ TEST(Pushrules, NormalMessageDoesNotHighlight) mtx::pushrules::PushRuleEvaluator evaluator{ruleset.global}; mtx::pushrules::PushRuleEvaluator::RoomContext ctx{}; - auto actions = evaluator.evaluate({text}, ctx); + auto actions = evaluator.evaluate({text}, ctx, {}); auto notifies = [](const std::vector<mtx::pushrules::actions::Action> &a) { for (const auto &action : a) { @@ -1888,3 +1889,451 @@ TEST(Pushrules, NormalMessageDoesNotHighlight) mtx::pushrules::actions::Action{mtx::pushrules::actions::set_tweak_highlight{}}); } } + +TEST(Pushrules, RelatedEventMatch) +{ + json raw_rule = R"( +{ + "global": { + "content": [ + ], + "override": [ + { + "actions": [ + "dont_notify" + ], + "default": true, + "enabled": false, + "rule_id": ".m.rule.master" + }, + { + "actions": [ + "notify", + { + "set_tweak": "highlight" + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "conditions": [ + { + "key": "sender", + "kind": "im.nheko.msc3664.related_event_match", + "pattern": "@deepbluev7:neko.dev", + "rel_type": "m.in_reply_to" + } + ], + "default": true, + "rule_id": ".im.nheko.msc3664.reply" + } + ], + "room": [ + ], + "sender": [ + ], + "underride": [ + ] +} +})"_json; + + mtx::pushrules::GlobalRuleset ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>(); + + json raw_event = R"( +{ + "content": { + "body": "You can hide them in the room settings", + "msgtype": "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de" + }, + "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de", + "rel_type": "m.thread" + } + }, + "event_id": "$mol6Bt546FLBNM7WgTj8mGTAieS8ZsX3JLZ2MH9qQwg", + "origin_server_ts": 1666006933326, + "room_id": "!UbCmIlGTHNIgIRZcpt:nheko.im", + "sender": "@nico:neko.dev", + "type": "m.room.message" +})"_json; + json related_raw_event = R"( +{ + "content": { + "body": "You can hide them in the room settings", + "msgtype": "m.text" + }, + "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de", + "origin_server_ts": 1666006933326, + "room_id": "!UbCmIlGTHNIgIRZcpt:nheko.im", + "sender": "@deepbluev7:neko.dev", + "type": "m.room.message" +})"_json; + + auto text = raw_event.get<mtx::events::RoomEvent<mtx::events::msg::Text>>(); + auto related_text = related_raw_event.get<mtx::events::RoomEvent<mtx::events::msg::Text>>(); + mtx::pushrules::PushRuleEvaluator evaluator{ruleset.global}; + mtx::pushrules::PushRuleEvaluator::RoomContext ctx{}; + + auto actions = evaluator.evaluate({text}, + ctx, + { + {text.content.relations.relations.at(0), {related_text}}, + {text.content.relations.relations.at(1), {related_text}}, + }); + + auto notifies = [](const std::vector<mtx::pushrules::actions::Action> &a) { + for (const auto &action : a) { + if (action == mtx::pushrules::actions::Action{mtx::pushrules::actions::notify{}}) { + return true; + } + } + return false; + }; + + EXPECT_TRUE( + notifies(evaluator.evaluate({text}, + ctx, + { + {text.content.relations.relations.at(0), {related_text}}, + {text.content.relations.relations.at(1), {related_text}}, + }))); + EXPECT_FALSE(notifies(evaluator.evaluate({text}, ctx, {}))); + + raw_rule = R"( +{ + "global": { + "content": [ + ], + "override": [ + { + "actions": [ + "dont_notify" + ], + "default": true, + "enabled": false, + "rule_id": ".m.rule.master" + }, + { + "actions": [ + "notify", + { + "set_tweak": "highlight" + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "conditions": [ + { + "key": "sender", + "kind": "im.nheko.msc3664.related_event_match", + "pattern": "@deepbluev7:neko.dev", + "rel_type": "m.thread" + } + ], + "default": true, + "rule_id": ".im.nheko.msc3664.reply" + } + ], + "room": [ + ], + "sender": [ + ], + "underride": [ + ] +} +})"_json; + + ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>(); + mtx::pushrules::PushRuleEvaluator evaluator1{ruleset.global}; + EXPECT_TRUE( + notifies(evaluator1.evaluate({text}, + ctx, + { + {text.content.relations.relations.at(0), {related_text}}, + {text.content.relations.relations.at(1), {related_text}}, + }))); + EXPECT_FALSE(notifies(evaluator1.evaluate({text}, ctx, {}))); + + raw_rule = R"( +{ + "global": { + "content": [ + ], + "override": [ + { + "actions": [ + "dont_notify" + ], + "default": true, + "enabled": false, + "rule_id": ".m.rule.master" + }, + { + "actions": [ + "notify", + { + "set_tweak": "highlight" + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "conditions": [ + { + "kind": "im.nheko.msc3664.related_event_match", + "rel_type": "m.thread" + } + ], + "default": true, + "rule_id": ".im.nheko.msc3664.reply" + } + ], + "room": [ + ], + "sender": [ + ], + "underride": [ + ] +} +})"_json; + + ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>(); + mtx::pushrules::PushRuleEvaluator evaluator2{ruleset.global}; + EXPECT_TRUE( + notifies(evaluator2.evaluate({text}, + ctx, + { + {text.content.relations.relations.at(0), {related_text}}, + {text.content.relations.relations.at(1), {related_text}}, + }))); + EXPECT_FALSE(notifies(evaluator2.evaluate({text}, ctx, {}))); + + raw_rule = R"( +{ + "global": { + "content": [ + ], + "override": [ + { + "actions": [ + "dont_notify" + ], + "default": true, + "enabled": false, + "rule_id": ".m.rule.master" + }, + { + "actions": [ + "notify", + { + "set_tweak": "highlight" + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "conditions": [ + { + "key": "sender", + "kind": "im.nheko.msc3664.related_event_match", + "pattern": "@deepbluev8:neko.dev", + "rel_type": "m.in_reply_to" + } + ], + "default": true, + "rule_id": ".im.nheko.msc3664.reply" + } + ], + "room": [ + ], + "sender": [ + ], + "underride": [ + ] +} +})"_json; + + ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>(); + mtx::pushrules::PushRuleEvaluator evaluator3{ruleset.global}; + EXPECT_FALSE( + notifies(evaluator3.evaluate({text}, + ctx, + { + {text.content.relations.relations.at(0), {related_text}}, + {text.content.relations.relations.at(1), {related_text}}, + }))); + EXPECT_FALSE(notifies(evaluator3.evaluate({text}, ctx, {}))); + + raw_event = R"( +{ + "content": { + "body": "You can hide them in the room settings", + "msgtype": "m.text", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de" + }, + "is_falling_back": true, + "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de", + "rel_type": "m.thread" + } + }, + "event_id": "$mol6Bt546FLBNM7WgTj8mGTAieS8ZsX3JLZ2MH9qQwg", + "origin_server_ts": 1666006933326, + "room_id": "!UbCmIlGTHNIgIRZcpt:nheko.im", + "sender": "@nico:neko.dev", + "type": "m.room.message" +})"_json; + text = raw_event.get<mtx::events::RoomEvent<mtx::events::msg::Text>>(); + EXPECT_FALSE( + notifies(evaluator.evaluate({text}, + ctx, + { + {text.content.relations.relations.at(0), {related_text}}, + {text.content.relations.relations.at(1), {related_text}}, + }))); + EXPECT_FALSE(notifies(evaluator.evaluate({text}, ctx, {}))); + + raw_rule = R"( +{ + "global": { + "content": [ + ], + "override": [ + { + "actions": [ + "dont_notify" + ], + "default": true, + "enabled": false, + "rule_id": ".m.rule.master" + }, + { + "actions": [ + "notify", + { + "set_tweak": "highlight" + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "conditions": [ + { + "key": "sender", + "kind": "im.nheko.msc3664.related_event_match", + "pattern": "@deepbluev7:neko.dev", + "rel_type": "m.in_reply_to", + "include_fallback": true + } + ], + "default": true, + "rule_id": ".im.nheko.msc3664.reply" + } + ], + "room": [ + ], + "sender": [ + ], + "underride": [ + ] +} +})"_json; + + ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>(); + mtx::pushrules::PushRuleEvaluator evaluator4{ruleset.global}; + EXPECT_TRUE( + notifies(evaluator4.evaluate({text}, + ctx, + { + {text.content.relations.relations.at(0), {related_text}}, + {text.content.relations.relations.at(1), {related_text}}, + }))); + EXPECT_FALSE(notifies(evaluator4.evaluate({text}, ctx, {}))); + + raw_rule = R"( +{ + "global": { + "content": [ + ], + "override": [ + { + "actions": [ + "dont_notify" + ], + "default": true, + "enabled": false, + "rule_id": ".m.rule.master" + }, + { + "actions": [ + "notify", + { + "set_tweak": "highlight" + }, + { + "set_tweak": "sound", + "value": "default" + } + ], + "conditions": [ + { + "key": "sender", + "kind": "im.nheko.msc3664.related_event_match", + "pattern": "@deepbluev7:neko.dev", + "rel_type": "m.in_reply_to" + } + ], + "default": true, + "rule_id": ".im.nheko.msc3664.reply" + } + ], + "room": [ + ], + "sender": [ + ], + "underride": [ + ] +} +})"_json; + + raw_event = R"( +{ + "content": { + "body": "You can hide them in the room settings", + "msgtype": "m.emote", + "m.relates_to": { + "m.in_reply_to": { + "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de" + }, + "event_id": "$aaaaaaaaaaaaaaaaaaaa:bc.de", + "rel_type": "m.thread" + } + }, + "event_id": "$mol6Bt546FLBNM7WgTj8mGTAieS8ZsX3JLZ2MH9qQwg", + "origin_server_ts": 1666006933326, + "room_id": "!UbCmIlGTHNIgIRZcpt:nheko.im", + "sender": "@nico:neko.dev", + "type": "m.room.message" +})"_json; + auto emote = raw_event.get<mtx::events::RoomEvent<mtx::events::msg::Emote>>(); + + ruleset = raw_rule.get<mtx::pushrules::GlobalRuleset>(); + mtx::pushrules::PushRuleEvaluator evaluator5{ruleset.global}; + EXPECT_TRUE( + notifies(evaluator5.evaluate({emote}, + ctx, + { + {emote.content.relations.relations.at(0), {related_text}}, + {emote.content.relations.relations.at(1), {related_text}}, + }))); + EXPECT_FALSE(notifies(evaluator5.evaluate({emote}, ctx, {}))); +}