Page MenuHomePhorge

push-rules-desc.cpp
No OneTemporary

Size
11 KB
Referenced Files
None
Subscribers
None

push-rules-desc.cpp

/*
* 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"; }))) {
cast(validatedAct, act, "value", identValidate(&json::is_string));
} else if (cast(validatedAct, act, "set_tweak", identValidate([](const auto &t) { return t == "highlight"; }))) {
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");
if (std::find(actions.begin(), actions.end(), "notify") != actions.end()) {
return {true};
}
return {false};
}
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};
}
}

File Metadata

Mime Type
text/x-c++
Expires
Thu, Oct 2, 2:28 AM (20 h, 54 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
471558
Default Alt Text
push-rules-desc.cpp (11 KB)

Event Timeline