Page MenuHomePhorge

No OneTemporary

Size
37 KB
Referenced Files
None
Subscribers
None
diff --git a/src/client/push-rules-desc.cpp b/src/client/push-rules-desc.cpp
index 7a6d3a5..1e0db38 100644
--- a/src/client/push-rules-desc.cpp
+++ b/src/client/push-rules-desc.cpp
@@ -1,314 +1,337 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <charconv>
#include <boost/algorithm/string/regex.hpp>
#include <immer/array.hpp>
#include <event.hpp>
#include <json-utils.hpp>
#include <room/room-model.hpp>
#include <validator.hpp>
#include "push-rules-desc-p.hpp"
namespace Kazv
{
static immer::array<std::string> ruleSets = {
"override",
"content",
"room",
"sender",
"underride",
};
// https://spec.matrix.org/v1.8/appendices/#dot-separated-property-paths
static const auto splitRegex = boost::regex("(?<!\\\\)\\.");
static const auto unescapeRegex = boost::regex("\\\\(\\\\|\\.)");
std::vector<std::string> splitPath(std::string path)
{
std::vector<std::string> ret;
boost::algorithm::split_regex(ret, path, splitRegex);
for (auto &part : ret) {
part = boost::regex_replace(part, unescapeRegex, "$1");
}
return ret;
}
static const auto metaCharsRegex = boost::regex("([*])|([?])|([.^$|()\\[\\]{}+\\\\])");
bool matchGlob(std::string target, std::string pattern)
{
auto matchRegex = boost::regex_replace(pattern, metaCharsRegex, "(?1.*:)(?2.:)(?3\\\\$3:)", boost::regex_constants::format_all);
return boost::regex_match(target, boost::regex(matchRegex, boost::regex::icase));
}
static bool isConditionKind(const json &kind)
{
return kind == "event_match"
|| kind == "event_property_is"
|| kind == "event_property_contains"
|| kind == "room_member_count";
}
static const auto memberCountIsRegex = boost::regex("(>|==|<|>=|<=)?([0-9]+)");
static bool isMemberCountIsValid(const json &is)
{
auto isStr = is.template get<std::string>();
return is.is_string() && boost::regex_match(isStr, memberCountIsRegex);
}
static std::function<bool(int)> parseMemberCountIs(const json &is)
{
boost::smatch m;
auto isStr = is.template get<std::string>();
auto res = boost::regex_match(isStr, m, memberCountIsRegex);
auto targetStr = m[2].str();
int target;
std::from_chars(targetStr.data(), targetStr.data() + targetStr.size(), target);
if (m[1] == "" || m[1] == "==") {
return [target](auto memberCount) { return memberCount == target; };
} else if (m[1] == ">") {
return [target](auto memberCount) { return memberCount > target; };
} else if (m[1] == "<") {
return [target](auto memberCount) { return memberCount < target; };
} else if (m[1] == ">=") {
return [target](auto memberCount) { return memberCount >= target; };
} else if (m[1] == "<=") {
return [target](auto memberCount) { return memberCount <= target; };
} else {
// shouldn't be here
return [](auto) { return false; };
}
}
static std::pair<bool, json> validateCondition(const json &cond)
{
json validatedCond = json::object();
if (!(
cond.is_object()
&& cast(validatedCond, cond, "kind", identValidate(&isConditionKind))
)) {
return {false, json()};
}
if (validatedCond["kind"] == "event_match") {
if (!(
cast(validatedCond, cond, "key", identValidate(&json::is_string))
&& cast(validatedCond, cond, "pattern", identValidate(&json::is_string))
)) {
return {false, json()};
}
} else if (validatedCond["kind"] == "event_property_is" || validatedCond["kind"] == "event_property_contains") {
if (!(
cast(validatedCond, cond, "key", identValidate(&json::is_string))
&& cast(validatedCond, cond, "value", identValidate(&isNonCompoundCanonicalJsonValue))
)) {
return {false, json()};
}
} else if (validatedCond["kind"] == "room_member_count") {
if (!(cast(validatedCond, cond, "is", identValidate(&isMemberCountIsValid)))) {
return {false, json()};
}
} else {
return {false, json()};
}
return {true, validatedCond};
}
static std::pair<bool, json> validateAction(const json &act)
{
if (act == "notify") {
return {true, act};
} else if (act.is_object()) {
json validatedAct = json::object();
if (cast(validatedAct, act, "set_tweak", identValidate([](const auto &t) { return t == "sound"; }))) {
+ validatedAct["value"] = "default";
cast(validatedAct, act, "value", identValidate(&json::is_string));
} else if (cast(validatedAct, act, "set_tweak", identValidate([](const auto &t) { return t == "highlight"; }))) {
+ validatedAct["value"] = true;
cast(validatedAct, act, "value", identValidate(&json::is_boolean));
}
return {true, validatedAct};
}
return {false, json()};
}
std::pair<bool, json> validateRule(std::string ruleSetName, const json &rule)
{
json ret = json::object();
if (!(
cast(ret, rule, "rule_id", identValidate(&json::is_string))
&& cast(ret, rule, "enabled", identValidate(&json::is_boolean))
&& cast(ret, rule, "default", identValidate(&json::is_boolean))
&& (!(ruleSetName == "override" || ruleSetName == "underride")
|| castArray(
ret,
makeDefaultValue(rule, "conditions", json::array()),
"conditions",
validateCondition,
CastArrayStrategy::FailAll
))
&& castArray(ret, rule, "actions", validateAction, CastArrayStrategy::IgnoreInvalid)
)) {
return {false, json()};
}
return {true, ret};
}
Event validatePushRules(const Event &e)
{
json ret{
{"type", "m.push_rules"},
{"content", {
{"global", {
{"override", {}},
{"content", {}},
{"room", {}},
{"sender", {}},
{"underride", {}},
}},
}},
};
const auto content = e.content().get();
for (const auto &ruleSetName : ruleSets) {
auto rules = getInJson(content, std::array<std::string, 2>{"global", ruleSetName});
castArray(
ret["content"],
content,
nlohmann::json_pointer<std::string>("/global/" + ruleSetName),
[ruleSetName](const json &rule) {
return validateRule(ruleSetName, rule);
},
CastArrayStrategy::IgnoreInvalid
);
}
return Event(ret);
}
PushRulesDescPrivate::PushRulesDescPrivate(const Event &e)
: pushRulesEvent(validatePushRules(e))
{}
std::optional<std::pair<std::string, json>> PushRulesDescPrivate::matchRule(const Event &e, const RoomModel &room) const
{
for (const auto &ruleSetName : ruleSets) {
auto matched = matchRuleSet(ruleSetName, e, room);
if (matched.has_value()) {
return matched;
}
}
return std::nullopt;
}
std::optional<std::pair<std::string, json>> PushRulesDescPrivate::matchRuleSet(std::string ruleSetName, const Event &e, const RoomModel &room) const
{
auto rules = pushRulesEvent.content().get().at("global").at(ruleSetName);
for (const auto &rule : rules) {
if (matchP(ruleSetName, rule, e, room)) {
return std::make_pair(ruleSetName, rule);
}
}
return std::nullopt;
}
bool PushRulesDescPrivate::matchP(std::string ruleSetName, const json &rule, const Event &e, const RoomModel &room) const
{
if (!rule.at("enabled").template get<bool>()) {
return false;
}
if (ruleSetName == "override" || ruleSetName == "underride") {
return std::all_of(
rule.at("conditions").begin(),
rule.at("conditions").end(),
[&e, &room](const auto &condition) mutable {
if (condition.is_object()) {
if (condition.at("kind") == "event_property_is") {
auto path = splitPath(condition.at("key"));
auto valueInEvent = getInJson(e.originalJson().get(), path);
if (!valueInEvent.has_value() || !isNonCompoundCanonicalJsonValue(valueInEvent.value())) {
return false;
}
return valueInEvent.value() == condition.at("value");
} else if (condition.at("kind") == "event_match") {
auto path = splitPath(condition.at("key"));
auto valueInEvent = getInJson(e.originalJson().get(), path);
if (!valueInEvent.has_value() || !valueInEvent.value().is_string()) {
return false;
}
auto valueStr = valueInEvent.value().template get<std::string>();
auto patternStr = condition.at("pattern").template get<std::string>();
return matchGlob(valueStr, patternStr);
} else if (condition.at("kind") == "event_property_contains") {
auto path = splitPath(condition.at("key"));
auto valueInEvent = getInJson(e.originalJson().get(), path);
if (!valueInEvent.has_value() || !valueInEvent.value().is_array()) {
return false;
}
auto vals = valueInEvent.value();
return std::any_of(vals.begin(), vals.end(), [cond=condition.at("value")](const auto &val) {
return isNonCompoundCanonicalJsonValue(val) && val == cond;
});
} else if (condition.at("kind") == "room_member_count") {
auto is = condition.at("is");
return parseMemberCountIs(is)(room.joinedMemberCount);
}
}
return false;
}
);
}
return false;
}
PushAction PushRulesDescPrivate::handleRule(std::string ruleSetName, const json &rule, const Event &e, const RoomModel &room) const
{
auto actions = rule.at("actions");
+ auto res = PushAction{
+ /* shouldNotify */ false,
+ /* sound */ std::nullopt,
+ /* shouldHighlight */ false,
+ };
if (std::find(actions.begin(), actions.end(), "notify") != actions.end()) {
- return {true};
+ res.shouldNotify = true;
}
- return {false};
+ if (auto it = std::find_if(
+ actions.begin(), actions.end(), [](const auto &a) {
+ return a.is_object()
+ && a.contains("set_tweak")
+ && a["set_tweak"] == "sound";
+ }); it != actions.end()) {
+ res.sound = (*it)["value"].template get<std::string>();
+ }
+ if (auto it = std::find_if(
+ actions.begin(), actions.end(), [](const auto &a) {
+ return a.is_object()
+ && a.contains("set_tweak")
+ && a["set_tweak"] == "highlight";
+ }); it != actions.end()) {
+ res.shouldHighlight = (*it)["value"].template get<bool>();
+ }
+ return res;
}
PushRulesDesc::PushRulesDesc()
: m_d()
{
}
PushRulesDesc::PushRulesDesc(const Event &pushRulesEvent)
: m_d(new PushRulesDescPrivate{pushRulesEvent})
{
}
PushRulesDesc::~PushRulesDesc() = default;
KAZV_DEFINE_COPYABLE_UNIQUE_PTR(PushRulesDesc, m_d);
bool PushRulesDesc::valid() const
{
return !!m_d;
}
PushAction PushRulesDesc::handle(const Event &e, const RoomModel &room) const
{
auto ruleOpt = m_d->matchRule(e, room);
if (ruleOpt.has_value()) {
auto [ruleSetName, rule] = ruleOpt.value();
return m_d->handleRule(ruleSetName, rule, e, room);
}
return {false};
}
}
diff --git a/src/client/push-rules-desc.hpp b/src/client/push-rules-desc.hpp
index 5549bfc..e06071a 100644
--- a/src/client/push-rules-desc.hpp
+++ b/src/client/push-rules-desc.hpp
@@ -1,63 +1,70 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#pragma once
#include <libkazv-config.hpp>
#include <memory>
+#include <optional>
#include <copy-helper.hpp>
namespace Kazv
{
struct RoomModel;
class Event;
struct PushRulesDescPrivate;
/**
* Describe what actions are to be taken for a specific event.
*/
struct PushAction
{
/// Whether the client should notify about this event.
bool shouldNotify;
+ /// The sound of the notification as defined in the spec.
+ /// If the notification should not ring, it is set as
+ /// `std::nullopt`.
+ std::optional<std::string> sound;
+ /// Whether the client should highlight the event.
+ bool shouldHighlight;
};
class PushRulesDesc
{
public:
/**
* Construct an invalid PushRulesDesc.
*/
PushRulesDesc();
/**
* Construct a PushRulesDesc from pushRulesEvent.
*/
PushRulesDesc(const Event &pushRulesEvent);
~PushRulesDesc();
KAZV_DECLARE_COPYABLE(PushRulesDesc)
/**
* Check whether this PushRulesDesc is valid.
*/
bool valid() const;
/**
* Check whether we should send a notification about event `e`.
*
* @param e The event to check.
* @param room The model of the room where the event is in.
* @return Action about event `e`.
*/
PushAction handle(const Event &e, const RoomModel &room) const;
private:
std::unique_ptr<PushRulesDescPrivate> m_d;
};
}
diff --git a/src/tests/client/push-rules-desc-test.cpp b/src/tests/client/push-rules-desc-test.cpp
index 3a080d1..4c2a651 100644
--- a/src/tests/client/push-rules-desc-test.cpp
+++ b/src/tests/client/push-rules-desc-test.cpp
@@ -1,493 +1,553 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <catch2/catch_test_macros.hpp>
#include <push-rules-desc-p.hpp>
#include <factory.hpp>
#include "push-rules-test-util.hpp"
using namespace Kazv;
using namespace Kazv::Factory;
static Event makePushRulesEvent(const json &pushRulesContent)
{
return makeEvent(
withEventContent(pushRulesContent)
| withEventType("m.push_rules")
);
}
TEST_CASE("splitPath()", "[client][push-rules]")
{
REQUIRE(splitPath("content.body") == std::vector<std::string>{"content", "body"});
REQUIRE(splitPath("content.m\\.relates_to") == std::vector<std::string>{"content", "m.relates_to"});
REQUIRE(splitPath("content.m\\\\foo") == std::vector<std::string>{"content", "m\\foo"});
REQUIRE(splitPath("content.m\\x") == std::vector<std::string>{"content", "m\\x"});
}
TEST_CASE("matchGlob()", "[client][push-rules]")
{
REQUIRE(matchGlob("m.notice", "m.notice"));
REQUIRE(!matchGlob("m.notice", "m.+notice"));
REQUIRE(!matchGlob("m.notice", "m.(notice)"));
REQUIRE(!matchGlob("mxnotice", "m.notice"));
REQUIRE(!matchGlob("m.notice", "^m.notice"));
REQUIRE(!matchGlob("m.notice", "m.notice$"));
REQUIRE(!matchGlob("m.notice", "m.notice|m.notice"));
REQUIRE(!matchGlob("m.notice", "m.notic[e]"));
REQUIRE(!matchGlob("m.notice", "m.notic\\e"));
REQUIRE(matchGlob("m\nnotice", "m?notice"));
REQUIRE(matchGlob("Lunch plans", "lunc?*"));
REQUIRE(matchGlob("LUNCH", "lunc?*"));
REQUIRE(!matchGlob(" lunch", "lunc?*"));
REQUIRE(!matchGlob("lunc", "lunc?*"));
}
TEST_CASE("validateRule()", "[client][push-rules]")
{
json rule = masterRule;
SECTION("minimum push rule")
{
auto validated = validateRule("override", rule);
REQUIRE(validated == std::make_pair(true, rule));
}
SECTION("strips extra properties")
{
rule["something"] = "else";
auto validated = validateRule("override", rule);
REQUIRE(validated == std::make_pair(true, masterRule));
}
SECTION("must have rule_id")
{
rule.erase("rule_id");
auto validated = validateRule("override", rule);
REQUIRE(!validated.first);
}
SECTION("must have default")
{
rule.erase("default");
auto validated = validateRule("override", rule);
REQUIRE(!validated.first);
}
SECTION("must have enabled")
{
rule.erase("enabled");
auto validated = validateRule("override", rule);
REQUIRE(!validated.first);
}
SECTION("must have conditions array")
{
rule["conditions"] = json::object();
auto validated = validateRule("override", rule);
REQUIRE(!validated.first);
}
SECTION("add empty array if conditions not present")
{
rule.erase("conditions");
auto validated = validateRule("override", rule);
REQUIRE(validated.first);
REQUIRE(validated.second == masterRule);
}
SECTION("reject if there is unknown condition")
{
rule["conditions"] = json::array({{{"kind", "moe.kazv.mxc.unknown-condition"}}});
auto validated = validateRule("override", rule);
REQUIRE(!validated.first);
}
SECTION("event_match condition")
{
rule["conditions"] = json::array({{
{"kind", "event_match"},
{"key", "some"},
{"pattern", "patt*n"},
}});
auto validated = validateRule("override", rule);
REQUIRE(validated.first);
REQUIRE(validated.second == rule);
auto ruleWithExtraProp = rule;
ruleWithExtraProp["conditions"][0]["some"] = "thing";
validated = validateRule("override", ruleWithExtraProp);
REQUIRE(validated.first);
REQUIRE(validated.second == rule);
}
SECTION("room_member_count condition")
{
rule["conditions"] = json::array({{
{"kind", "room_member_count"},
{"is", "2"},
}});
auto validated = validateRule("override", rule);
REQUIRE(validated.first);
REQUIRE(validated.second == rule);
rule["conditions"][0]["is"] = "==2";
validated = validateRule("override", rule);
REQUIRE(validated.first);
REQUIRE(validated.second == rule);
rule["conditions"][0]["is"] = ">2";
validated = validateRule("override", rule);
REQUIRE(validated.first);
REQUIRE(validated.second == rule);
rule["conditions"][0]["is"] = "2!";
validated = validateRule("override", rule);
REQUIRE(!validated.first);
rule["conditions"][0]["is"] = "!=2";
validated = validateRule("override", rule);
REQUIRE(!validated.first);
rule["conditions"][0]["is"] = "017";
validated = validateRule("override", rule);
REQUIRE(validated.first);
REQUIRE(validated.second == rule);
}
+
+ SECTION("action set_tweak sound")
+ {
+ rule["actions"] = json::array({{
+ {"set_tweak", "sound"},
+ {"value", "default"},
+ }});
+
+ auto validated = validateRule("override", rule);
+ REQUIRE(validated.first);
+ REQUIRE(validated.second == rule);
+
+ auto ruleWithoutValue = rule;
+ ruleWithoutValue["actions"][0].erase("value");
+
+ validated = validateRule("override", ruleWithoutValue);
+ REQUIRE(validated.first);
+ REQUIRE(validated.second == rule);
+ }
+
+ SECTION("action set_tweak highlight")
+ {
+ rule["actions"] = json::array({{
+ {"set_tweak", "highlight"},
+ {"value", true},
+ }});
+
+ auto validated = validateRule("override", rule);
+ REQUIRE(validated.first);
+ REQUIRE(validated.second == rule);
+
+ auto ruleWithoutValue = rule;
+ ruleWithoutValue["actions"][0].erase("value");
+
+ validated = validateRule("override", ruleWithoutValue);
+ REQUIRE(validated.first);
+ REQUIRE(validated.second == rule);
+ }
}
TEST_CASE("validatePushRules()", "[client][push-rules]")
{
json content = {
{"global", {
{"override", {masterRule, suppressNoticesRule, isUserMentionRule}},
{"content", {}},
{"room", {}},
{"sender", {}},
{"underride", {}},
}},
};
auto e = Event{json{
{"type", "m.push_rules"},
{"content", content},
}};
SECTION("nothing unrecognized")
{
auto validated = validatePushRules(e);
REQUIRE(validated == e);
}
SECTION("extra param in content")
{
content["something"] = "else";
auto extra = Event{json{
{"type", "m.push_rules"},
{"content", content},
}};
auto validated = validatePushRules(extra);
REQUIRE(validated == e);
}
SECTION("extra param in global")
{
content["global"]["something"] = "else";
auto extra = Event{json{
{"type", "m.push_rules"},
{"content", content},
}};
auto validated = validatePushRules(extra);
REQUIRE(validated == e);
}
SECTION("extra param in rule")
{
content["global"]["override"][0]["something"] = "else";
auto extra = Event{json{
{"type", "m.push_rules"},
{"content", content},
}};
auto validated = validatePushRules(extra);
REQUIRE(validated == e);
}
SECTION("extra param in condition")
{
content["global"]["override"][1]["conditions"][0]["something"] = "else";
auto extra = Event{json{
{"type", "m.push_rules"},
{"content", content},
}};
auto validated = validatePushRules(extra);
REQUIRE(validated == e);
}
SECTION("unrecognized condition")
{
content["global"]["override"][1]["conditions"].push_back(json::object({{"kind", "moe.kazv.mxc.unknown-condition"}}));
auto extra = Event{json{
{"type", "m.push_rules"},
{"content", content},
}};
content["global"]["override"].erase(1);
auto expected = Event{json{
{"type", "m.push_rules"},
{"content", content},
}};
auto validated = validatePushRules(extra);
REQUIRE(validated == expected);
}
SECTION("unrecognized action")
{
content["global"]["override"][1]["actions"].push_back("moe.kazv.mxc.unknown-action");
auto extra = Event{json{
{"type", "m.push_rules"},
{"content", content},
}};
auto validated = validatePushRules(extra);
REQUIRE(validated == e);
}
}
TEST_CASE("PushRulesDescPrivate::matchP()", "[client][push-rules]")
{
PushRulesDescPrivate rp{Event()};
auto room = makeRoom();
SECTION("master rule")
{
REQUIRE(rp.matchP("override", masterRule, makeEvent(), room));
}
SECTION("suppress notices rule")
{
auto notice = makeEvent(withEventContent(json{{"msgtype", "m.notice"}}));
REQUIRE(rp.matchP("override", suppressNoticesRule, notice, room));
auto notNotice = makeEvent(withEventContent(json{{"msgtype", "m.noticexxx"}}));
REQUIRE(!rp.matchP("override", suppressNoticesRule, notNotice, room));
}
SECTION("invite for me rule")
{
auto invite = makeEvent(
withEventContent(json{{"membership", "invite"}})
| withEventType("m.room.member")
| withStateKey("@foo:example.com")
);
REQUIRE(rp.matchP("override", inviteForMeRule, invite, room));
auto inviteForOthers = makeEvent(
withEventContent(json{{"membership", "invite"}})
| withEventType("m.room.member")
| withStateKey("@foo:example.com.tw")
);
REQUIRE(!rp.matchP("override", inviteForMeRule, inviteForOthers, room));
}
SECTION("member event rule")
{
auto member = makeEvent(
withEventContent(json{{"membership", "join"}})
| withEventType("m.room.member")
| withStateKey("@foo:example.com")
);
REQUIRE(rp.matchP("override", memberEventRule, member, room));
}
SECTION("is user mention rule")
{
auto mentionedEvent = makeEvent(
withEventContent(json{
{"m.mentions", {
{"user_ids", {"@foo:example.com"}}
}}
})
);
auto notMentionedEvent = makeEvent(
withEventContent(json{
{"m.mentions", {
{"user_ids", {"@foo:example.com.tw"}}
}}
})
);
REQUIRE(rp.matchP("override", isUserMentionRule, mentionedEvent, room));
REQUIRE(!rp.matchP("override", isUserMentionRule, notMentionedEvent, room));
}
SECTION("one to one rule")
{
auto event = makeEvent();
auto withJoinedMemberCount = [](auto count) {
return [count](RoomModel &r) {
r.joinedMemberCount = count;
};
};
auto withInvitedMemberCount = [](auto count) {
return [count](RoomModel &r) {
r.invitedMemberCount = count;
};
};
SECTION("no one in room")
{
REQUIRE(!rp.matchP("override", oneToOneRule, event, room));
}
SECTION("two people in room")
{
withJoinedMemberCount(2)(room);
REQUIRE(rp.matchP("override", oneToOneRule, event, room));
}
SECTION("two joined and one invited")
{
withJoinedMemberCount(2)(room);
withInvitedMemberCount(1)(room);
REQUIRE(rp.matchP("override", oneToOneRule, event, room));
}
SECTION("three joined")
{
withJoinedMemberCount(3)(room);
REQUIRE(!rp.matchP("override", oneToOneRule, event, room));
}
}
}
TEST_CASE("Push rules works with predefined rules", "[client][push-rules]")
{
auto rulesContent = emptyRulesContent;
auto room = makeRoom();
SECTION("master rule")
{
rulesContent["global"]["override"].push_back(masterRule);
rulesContent["global"]["underride"].push_back(catchAllRule);
auto pushRules = PushRulesDesc(makePushRulesEvent(rulesContent));
auto result = pushRules.handle(makeEvent(), room);
REQUIRE(!result.shouldNotify);
}
SECTION("suppress notices rule")
{
rulesContent["global"]["override"].push_back(suppressNoticesRule);
rulesContent["global"]["underride"].push_back(catchAllRule);
auto pushRules = PushRulesDesc(makePushRulesEvent(rulesContent));
auto result = pushRules.handle(makeEvent(withEventContent(json{{"msgtype", "m.notice"}})), room);
REQUIRE(!result.shouldNotify);
}
SECTION("invite for me rule")
{
rulesContent["global"]["override"].push_back(inviteForMeRule);
auto pushRules = PushRulesDesc(makePushRulesEvent(rulesContent));
auto result = pushRules.handle(makeEvent(
withEventContent(json{{"membership", "invite"}})
| withEventType("m.room.member")
| withStateKey("@foo:example.com")
), room);
REQUIRE(result.shouldNotify);
}
SECTION("member event rule")
{
rulesContent["global"]["override"].push_back(memberEventRule);
rulesContent["global"]["underride"].push_back(catchAllRule);
auto pushRules = PushRulesDesc(makePushRulesEvent(rulesContent));
auto result = pushRules.handle(makeEvent(
withEventContent(json{{"membership", "invite"}})
| withEventType("m.room.member")
| withStateKey("@foo:example.com")
), room);
REQUIRE(!result.shouldNotify);
}
SECTION("is user mention rule")
{
auto mentionedEvent = makeEvent(
withEventContent(json{
{"m.mentions", {
{"user_ids", {"@foo:example.com"}}
}}
})
);
auto notMentionedEvent = makeEvent(
withEventContent(json{
{"m.mentions", {
{"user_ids", {"@foo:example.com.tw"}}
}}
})
);
rulesContent["global"]["override"].push_back(isUserMentionRule);
auto pushRules = PushRulesDesc(makePushRulesEvent(rulesContent));
REQUIRE(pushRules.handle(mentionedEvent, room).shouldNotify);
REQUIRE(!pushRules.handle(notMentionedEvent, room).shouldNotify);
}
SECTION("one to one rule")
{
auto event = makeEvent();
rulesContent["global"]["override"].push_back(oneToOneRule);
auto pushRules = PushRulesDesc(makePushRulesEvent(rulesContent));
SECTION("no one in room")
{
REQUIRE(!pushRules.handle(event, room).shouldNotify);
}
SECTION("two people in room")
{
room.joinedMemberCount = 2;
REQUIRE(pushRules.handle(event, room).shouldNotify);
}
}
}
TEST_CASE("Push rules would not crash with broken rules", "[client][push-rules]")
{
auto rulesContent = emptyRulesContent;
auto room = makeRoom();
REQUIRE(!PushRulesDesc(Event()).handle(makeEvent(), room).shouldNotify);
SECTION("rule missing required param")
{
auto rule = catchAllRule;
rule.erase("rule_id");
rulesContent["global"]["override"].push_back(rule);
REQUIRE(!PushRulesDesc(makePushRulesEvent(rulesContent)).handle(makeEvent(), room).shouldNotify);
}
SECTION("rule containing invalid condition")
{
auto rule = catchAllRule;
rule["conditions"].push_back("something");
rulesContent["global"]["override"].push_back(rule);
REQUIRE(!PushRulesDesc(makePushRulesEvent(rulesContent)).handle(makeEvent(), room).shouldNotify);
}
SECTION("rule containing unknown action")
{
auto rule = catchAllRule;
rule["actions"].push_back("something");
rulesContent["global"]["override"].push_back(rule);
REQUIRE(PushRulesDesc(makePushRulesEvent(rulesContent)).handle(makeEvent(), room).shouldNotify);
}
+
+ SECTION("action set_tweak sound")
+ {
+ auto rule = catchAllRule;
+ rule["actions"].push_back(json{
+ {"set_tweak", "sound"},
+ {"value", "some"},
+ });
+ rulesContent["global"]["override"].push_back(rule);
+ REQUIRE(PushRulesDesc(makePushRulesEvent(rulesContent)).handle(makeEvent(), room).sound == "some");
+ }
+
+ SECTION("action set_tweak highlight")
+ {
+ auto rule = catchAllRule;
+ rule["actions"].push_back(json{
+ {"set_tweak", "highlight"},
+ {"value", true},
+ });
+ rulesContent["global"]["override"].push_back(rule);
+ REQUIRE(PushRulesDesc(makePushRulesEvent(rulesContent)).handle(makeEvent(), room).shouldHighlight);
+ }
}
diff --git a/src/tests/client/push-rules-test-util.hpp b/src/tests/client/push-rules-test-util.hpp
index 285466b..5880a6b 100644
--- a/src/tests/client/push-rules-test-util.hpp
+++ b/src/tests/client/push-rules-test-util.hpp
@@ -1,219 +1,220 @@
/*
* This file is part of libkazv.
* SPDX-FileCopyrightText: 2020-2023 tusooa <tusooa@kazv.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#include <libkazv-config.hpp>
#include <types.hpp>
using namespace Kazv;
// adapted from https://spec.matrix.org/v1.8/client-server-api/#default-override-rules
inline const auto masterRule = R"({
"rule_id": ".m.rule.master",
"default": true,
"enabled": true,
"conditions": [],
"actions": []
})"_json;
inline const auto suppressNoticesRule = R"({
"rule_id": ".m.rule.suppress_notices",
"default": true,
"enabled": true,
"conditions": [
{
"kind": "event_match",
"key": "content.msgtype",
"pattern": "m.notice"
}
],
"actions": []
})"_json;
inline const auto inviteForMeRule = R"({
"rule_id": ".m.rule.invite_for_me",
"default": true,
"enabled": true,
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
},
{
"key": "content.membership",
"kind": "event_match",
"pattern": "invite"
},
{
"key": "state_key",
"kind": "event_match",
"pattern": "@foo:example.com"
}
],
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
]
})"_json;
inline const auto memberEventRule = R"({
"rule_id": ".m.rule.member_event",
"default": true,
"enabled": true,
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.room.member"
}
],
"actions": []
})"_json;
inline const auto isUserMentionRule = R"({
"rule_id": ".m.rule.is_user_mention",
"default": true,
"enabled": true,
"conditions": [
{
"kind": "event_property_contains",
"key": "content.m\\.mentions.user_ids",
"value": "@foo:example.com"
}
],
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
},
{
- "set_tweak": "highlight"
+ "set_tweak": "highlight",
+ "value": true
}
]
})"_json;
inline const auto callRule = R"({
"rule_id": ".m.rule.call",
"default": true,
"enabled": true,
"conditions": [
{
"key": "type",
"kind": "event_match",
"pattern": "m.call.invite"
}
],
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "ring"
}
]
})"_json;
inline const auto encryptedOneToOneRule = R"({
"rule_id": ".m.rule.encrypted_room_one_to_one",
"default": true,
"enabled": true,
"conditions": [
{
"kind": "room_member_count",
"is": "2"
},
{
"kind": "event_match",
"key": "type",
"pattern": "m.room.encrypted"
}
],
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
]
})"_json;
inline const auto oneToOneRule = R"({
"rule_id": ".m.rule.room_one_to_one",
"default": true,
"enabled": true,
"conditions": [
{
"kind": "room_member_count",
"is": "2"
},
{
"kind": "event_match",
"key": "type",
"pattern": "m.room.message"
}
],
"actions": [
"notify",
{
"set_tweak": "sound",
"value": "default"
}
]
})"_json;
inline const auto messageRule = R"({
"rule_id": ".m.rule.message",
"default": true,
"enabled": true,
"conditions": [
{
"kind": "event_match",
"key": "type",
"pattern": "m.room.message"
}
],
"actions": [
"notify"
]
})"_json;
inline const auto encryptedRule = R"({
"rule_id": ".m.rule.encrypted",
"default": true,
"enabled": true,
"conditions": [
{
"kind": "event_match",
"key": "type",
"pattern": "m.room.encrypted"
}
],
"actions": [
"notify"
]
})"_json;
inline const auto catchAllRule = R"({
"rule_id": "moe.kazv.mxc.catch_all",
"default": true,
"enabled": true,
"conditions": [],
"actions": ["notify"]
})"_json;
inline const auto emptyRulesContent = json{
{"global", {
{"override", {}},
{"content", {}},
{"room", {}},
{"sender", {}},
{"underride", {}},
}},
};

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jan 19, 10:43 AM (3 h, 29 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55108
Default Alt Text
(37 KB)

Event Timeline