Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Experimental support for MSC3772 #12740

Merged
merged 18 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/12740.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Experimental support for [MSC3772](https://github.com/matrix-org/matrix-spec-proposals/pull/3772): Push rule for mutually related events.
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:

# MSC3786 (Add a default push rule to ignore m.room.server_acl events)
self.msc3786_enabled: bool = experimental.get("msc3786_enabled", False)

# MSC3772: A push rule for mutual relations.
self.msc3772_enabled: bool = experimental.get("msc3772_enabled", False)
14 changes: 14 additions & 0 deletions synapse/push/baserules.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ def make_base_prepend_rules(
{
"kind": "event_match",
"key": "content.body",
# Match the localpart of the requester's MXID.
"pattern_type": "user_localpart",
}
],
Expand Down Expand Up @@ -191,6 +192,7 @@ def make_base_prepend_rules(
"pattern": "invite",
"_cache_key": "_invite_member",
},
# Match the requester's MXID.
{"kind": "event_match", "key": "state_key", "pattern_type": "user_id"},
],
"actions": [
Expand Down Expand Up @@ -350,6 +352,18 @@ def make_base_prepend_rules(
{"set_tweak": "highlight", "value": False},
],
},
{
"rule_id": "global/underride/.org.matrix.msc3772.thread_reply",
"conditions": [
{
"kind": "org.matrix.msc3772.relation_match",
"rel_type": "m.thread",
# Match the requester's MXID.
"sender_type": "user_id",
}
],
"actions": ["notify", {"set_tweak": "highlight", "value": False}],
},
{
"rule_id": "global/underride/.m.rule.message",
"conditions": [
Expand Down
12 changes: 11 additions & 1 deletion synapse/push/bulk_push_rule_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ def __init__(self, hs: "HomeServer"):
resizable=False,
)

# Whether to support MSC3772 is supported.
self._relations_match_enabled = self.hs.config.experimental.msc3772_enabled

async def _get_rules_for_event(
self, event: EventBase, context: EventContext
) -> Dict[str, List[Dict[str, Any]]]:
Expand Down Expand Up @@ -211,8 +214,15 @@ async def action_for_event_by_user(
sender_power_level,
) = await self._get_power_levels_and_sender_level(event, context)

relations = await self.store.get_mutual_event_relations(event)
clokep marked this conversation as resolved.
Show resolved Hide resolved

evaluator = PushRuleEvaluatorForEvent(
event, len(room_members), sender_power_level, power_levels
event,
len(room_members),
sender_power_level,
power_levels,
relations,
self._relations_match_enabled,
)

# If the event is not a state event check if any users ignore the sender.
Expand Down
4 changes: 4 additions & 0 deletions synapse/push/clientformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ def format_push_rules_for_user(
elif pattern_type == "user_localpart":
c["pattern"] = user.localpart

sender_type = c.pop("sender_type", None)
if sender_type == "user_id":
c["sender"] = user.to_string()

rulearray = rules["global"][template_name]

template_rule = _rule_to_template(r)
Expand Down
48 changes: 47 additions & 1 deletion synapse/push/push_rule_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import logging
import re
from typing import Any, Dict, List, Mapping, Optional, Pattern, Tuple, Union
from typing import Any, Dict, List, Mapping, Optional, Pattern, Set, Tuple, Union

from matrix_common.regex import glob_to_regex, to_word_pattern

Expand Down Expand Up @@ -120,11 +120,15 @@ def __init__(
room_member_count: int,
sender_power_level: int,
power_levels: Dict[str, Union[int, Dict[str, int]]],
relations: Set[Tuple[str, str, str]],
relations_match_enabled: bool,
):
self._event = event
self._room_member_count = room_member_count
self._sender_power_level = sender_power_level
self._power_levels = power_levels
self._relations = relations
self._relations_match_enabled = relations_match_enabled

# Maps strings of e.g. 'content.body' -> event["content"]["body"]
self._value_cache = _flatten_dict(event)
Expand Down Expand Up @@ -188,6 +192,11 @@ def matches(
return _sender_notification_permission(
self._event, condition, self._sender_power_level, self._power_levels
)
elif (
condition["kind"] == "org.matrix.msc3772.relation_match"
and self._relations_match_enabled
):
return self._relation_match(condition, user_id)
else:
return True

Expand Down Expand Up @@ -256,6 +265,43 @@ def _contains_display_name(self, display_name: Optional[str]) -> bool:

return bool(r.search(body))

def _relation_match(self, condition: dict, user_id: str) -> bool:
"""
Check an "relation_match" push rule condition.

Args:
condition: The "event_match" push rule condition to match.
user_id: The user's MXID.

Returns:
True if the condition matches the event, False otherwise.
"""
rel_type_pattern = condition.get("rel_type")
sender_pattern = condition.get("sender")
if sender_pattern is None:
sender_type = condition.get("sender_type")
if sender_type == "user_id":
sender_pattern = user_id
type_pattern = condition.get("type")

if not rel_type_pattern and not sender_pattern and not type_pattern:
logger.warning("relation_match condition with nothing to match")
return False

# If any other relations matches, return True.
for relation in self._relations:
if rel_type_pattern and not _glob_matches(rel_type_pattern, relation[0]):
continue
if sender_pattern and not _glob_matches(sender_pattern, relation[1]):
continue
if type_pattern and not _glob_matches(type_pattern, relation[2]):
continue
# All values must have matched.
return True

# No relations matched.
return False


# Caches (string, is_glob, word_boundary) -> regex for push. See _glob_matches
regex_cache: LruCache[Tuple[str, bool, bool], Pattern] = LruCache(
Expand Down
5 changes: 5 additions & 0 deletions synapse/storage/databases/main/push_rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ def _is_experimental_rule_enabled(
and not experimental_config.msc3786_enabled
):
return False
if (
rule_id == "global/underride/.org.matrix.msc3772.thread_reply"
and not experimental_config.msc3772_enabled
):
return False
return True


Expand Down
37 changes: 36 additions & 1 deletion synapse/storage/databases/main/relations.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import attr

from synapse.api.constants import RelationTypes
from synapse.events import EventBase
from synapse.events import EventBase, relation_from_event
from synapse.storage._base import SQLBaseStore
from synapse.storage.database import LoggingTransaction, make_in_list_sql_clause
from synapse.storage.databases.main.stream import generate_pagination_where_clause
Expand Down Expand Up @@ -765,6 +765,41 @@ def _get_if_user_has_annotated_event(txn: LoggingTransaction) -> bool:
"get_if_user_has_annotated_event", _get_if_user_has_annotated_event
)

async def get_mutual_event_relations(
clokep marked this conversation as resolved.
Show resolved Hide resolved
self, event: EventBase
) -> Set[Tuple[str, str, str]]:
"""
Fetch event meta data for events which related to the same event as the given event.

If the given event has no relation information, returns an empty set.

Args:
event: The event to fetch relations for.

Returns:
A set of tuples of:
The relation type
The sender
The event type
"""
relation = relation_from_event(event)
if not relation:
return set()
clokep marked this conversation as resolved.
Show resolved Hide resolved

sql = """
SELECT relation_type, sender, type FROM event_relations
clokep marked this conversation as resolved.
Show resolved Hide resolved
INNER JOIN events USING (event_id)
WHERE relates_to_id = ?
"""

def _get_event_relations(txn: LoggingTransaction) -> Set[Tuple[str, str, str]]:
txn.execute(sql, (relation.parent_id,)) # type: ignore[union-attr]
return set(cast(List[Tuple[str, str, str]], txn.fetchall()))

return await self.db_pool.runInteraction(
"get_event_relations", _get_event_relations
)


class RelationsStore(RelationsWorkerStore):
pass
77 changes: 74 additions & 3 deletions tests/push/test_push_rule_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Dict, Optional, Union
from typing import Dict, Optional, Set, Tuple, Union

import frozendict

Expand All @@ -26,7 +26,12 @@


class PushRuleEvaluatorTestCase(unittest.TestCase):
def _get_evaluator(self, content: JsonDict) -> PushRuleEvaluatorForEvent:
def _get_evaluator(
self,
content: JsonDict,
relations: Optional[Set[Tuple[str, str, str]]] = None,
relations_match_enabled: bool = False,
) -> PushRuleEvaluatorForEvent:
event = FrozenEvent(
{
"event_id": "$event_id",
Expand All @@ -42,7 +47,12 @@ def _get_evaluator(self, content: JsonDict) -> PushRuleEvaluatorForEvent:
sender_power_level = 0
power_levels: Dict[str, Union[int, Dict[str, int]]] = {}
return PushRuleEvaluatorForEvent(
event, room_member_count, sender_power_level, power_levels
event,
room_member_count,
sender_power_level,
power_levels,
relations or set(),
relations_match_enabled,
)

def test_display_name(self) -> None:
Expand Down Expand Up @@ -276,3 +286,64 @@ def test_tweaks_for_actions(self) -> None:
push_rule_evaluator.tweaks_for_actions(actions),
{"sound": "default", "highlight": True},
)

def test_relation_match(self) -> None:
"""Test the relation_match push rule kind."""

# Check if the experimental feature is disabled.
evaluator = self._get_evaluator(
{}, {("m.annotation", "@user:test", "m.reaction")}
)
condition = {"kind": "relation_match"}
# Oddly, an unknown condition always matches.
self.assertTrue(evaluator.matches(condition, "@user:test", "foo"))

# A push rule evaluator with the experimental rule enabled.
evaluator = self._get_evaluator(
{}, {("m.annotation", "@user:test", "m.reaction")}, True
)

# Check just relation type.
condition = {
"kind": "org.matrix.msc3772.relation_match",
"rel_type": "m.annotation",
}
self.assertTrue(evaluator.matches(condition, "@user:test", "foo"))

# Check relation type and sender.
condition = {
"kind": "org.matrix.msc3772.relation_match",
"rel_type": "m.annotation",
"sender": "@user:test",
}
self.assertTrue(evaluator.matches(condition, "@user:test", "foo"))
condition = {
"kind": "org.matrix.msc3772.relation_match",
"rel_type": "m.annotation",
"sender": "@other:test",
}
self.assertFalse(evaluator.matches(condition, "@user:test", "foo"))

# Check relation type and event type.
condition = {
"kind": "org.matrix.msc3772.relation_match",
"rel_type": "m.annotation",
"type": "m.reaction",
}
self.assertTrue(evaluator.matches(condition, "@user:test", "foo"))

# Check just sender.
condition = {
"kind": "org.matrix.msc3772.relation_match",
"sender": "@user:test",
}
self.assertTrue(evaluator.matches(condition, "@user:test", "foo"))
condition = {
"kind": "org.matrix.msc3772.relation_match",
"sender": "@other:test",
}
self.assertFalse(evaluator.matches(condition, "@user:test", "foo"))

# Check glob.
condition = {"kind": "org.matrix.msc3772.relation_match", "sender": "@*:test"}
self.assertTrue(evaluator.matches(condition, "@user:test", "foo"))