Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F140031
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
37 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 10:43 AM (58 m, 48 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
55108
Default Alt Text
(37 KB)
Attached To
Mode
rL libkazv
Attached
Detach File
Event Timeline
Log In to Comment