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

Commit 88ce308

Browse files
authored
Experimental support for MSC3772 (#12740)
Implements the following behind an experimental configuration flag: * A new push rule kind for mutually related events. * A new default push rule (`.m.rule.thread_reply`) under an unstable prefix. This is missing part of MSC3772: * The `.m.rule.thread_reply_to_me` push rule, this depends on MSC3664 / #11804.
1 parent 0b3423f commit 88ce308

File tree

10 files changed

+287
-6
lines changed

10 files changed

+287
-6
lines changed

changelog.d/12740.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Experimental support for [MSC3772](https://github.com/matrix-org/matrix-spec-proposals/pull/3772): Push rule for mutually related events.

synapse/config/experimental.py

+3
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
8484

8585
# MSC3786 (Add a default push rule to ignore m.room.server_acl events)
8686
self.msc3786_enabled: bool = experimental.get("msc3786_enabled", False)
87+
88+
# MSC3772: A push rule for mutual relations.
89+
self.msc3772_enabled: bool = experimental.get("msc3772_enabled", False)

synapse/push/baserules.py

+14
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def make_base_prepend_rules(
139139
{
140140
"kind": "event_match",
141141
"key": "content.body",
142+
# Match the localpart of the requester's MXID.
142143
"pattern_type": "user_localpart",
143144
}
144145
],
@@ -191,6 +192,7 @@ def make_base_prepend_rules(
191192
"pattern": "invite",
192193
"_cache_key": "_invite_member",
193194
},
195+
# Match the requester's MXID.
194196
{"kind": "event_match", "key": "state_key", "pattern_type": "user_id"},
195197
],
196198
"actions": [
@@ -350,6 +352,18 @@ def make_base_prepend_rules(
350352
{"set_tweak": "highlight", "value": False},
351353
],
352354
},
355+
{
356+
"rule_id": "global/underride/.org.matrix.msc3772.thread_reply",
357+
"conditions": [
358+
{
359+
"kind": "org.matrix.msc3772.relation_match",
360+
"rel_type": "m.thread",
361+
# Match the requester's MXID.
362+
"sender_type": "user_id",
363+
}
364+
],
365+
"actions": ["notify", {"set_tweak": "highlight", "value": False}],
366+
},
353367
{
354368
"rule_id": "global/underride/.m.rule.message",
355369
"conditions": [

synapse/push/bulk_push_rule_evaluator.py

+69-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
import itertools
1617
import logging
17-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union
18+
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Set, Tuple, Union
1819

1920
import attr
2021
from prometheus_client import Counter
@@ -121,6 +122,9 @@ def __init__(self, hs: "HomeServer"):
121122
resizable=False,
122123
)
123124

125+
# Whether to support MSC3772 is supported.
126+
self._relations_match_enabled = self.hs.config.experimental.msc3772_enabled
127+
124128
async def _get_rules_for_event(
125129
self, event: EventBase, context: EventContext
126130
) -> Dict[str, List[Dict[str, Any]]]:
@@ -192,6 +196,60 @@ async def _get_power_levels_and_sender_level(
192196

193197
return pl_event.content if pl_event else {}, sender_level
194198

199+
async def _get_mutual_relations(
200+
self, event: EventBase, rules: Iterable[Dict[str, Any]]
201+
) -> Dict[str, Set[Tuple[str, str]]]:
202+
"""
203+
Fetch event metadata for events which related to the same event as the given event.
204+
205+
If the given event has no relation information, returns an empty dictionary.
206+
207+
Args:
208+
event_id: The event ID which is targeted by relations.
209+
rules: The push rules which will be processed for this event.
210+
211+
Returns:
212+
A dictionary of relation type to:
213+
A set of tuples of:
214+
The sender
215+
The event type
216+
"""
217+
218+
# If the experimental feature is not enabled, skip fetching relations.
219+
if not self._relations_match_enabled:
220+
return {}
221+
222+
# If the event does not have a relation, then cannot have any mutual
223+
# relations.
224+
relation = relation_from_event(event)
225+
if not relation:
226+
return {}
227+
228+
# Pre-filter to figure out which relation types are interesting.
229+
rel_types = set()
230+
for rule in rules:
231+
# Skip disabled rules.
232+
if "enabled" in rule and not rule["enabled"]:
233+
continue
234+
235+
for condition in rule["conditions"]:
236+
if condition["kind"] != "org.matrix.msc3772.relation_match":
237+
continue
238+
239+
# rel_type is required.
240+
rel_type = condition.get("rel_type")
241+
if rel_type:
242+
rel_types.add(rel_type)
243+
244+
# If no valid rules were found, no mutual relations.
245+
if not rel_types:
246+
return {}
247+
248+
# If any valid rules were found, fetch the mutual relations.
249+
return await self.store.get_mutual_event_relations(
250+
relation.parent_id, rel_types
251+
)
252+
195253
@measure_func("action_for_event_by_user")
196254
async def action_for_event_by_user(
197255
self, event: EventBase, context: EventContext
@@ -216,8 +274,17 @@ async def action_for_event_by_user(
216274
sender_power_level,
217275
) = await self._get_power_levels_and_sender_level(event, context)
218276

277+
relations = await self._get_mutual_relations(
278+
event, itertools.chain(*rules_by_user.values())
279+
)
280+
219281
evaluator = PushRuleEvaluatorForEvent(
220-
event, len(room_members), sender_power_level, power_levels
282+
event,
283+
len(room_members),
284+
sender_power_level,
285+
power_levels,
286+
relations,
287+
self._relations_match_enabled,
221288
)
222289

223290
# If the event is not a state event check if any users ignore the sender.

synapse/push/clientformat.py

+4
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ def format_push_rules_for_user(
4848
elif pattern_type == "user_localpart":
4949
c["pattern"] = user.localpart
5050

51+
sender_type = c.pop("sender_type", None)
52+
if sender_type == "user_id":
53+
c["sender"] = user.to_string()
54+
5155
rulearray = rules["global"][template_name]
5256

5357
template_rule = _rule_to_template(r)

synapse/push/push_rule_evaluator.py

+49-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import logging
1717
import re
18-
from typing import Any, Dict, List, Mapping, Optional, Pattern, Tuple, Union
18+
from typing import Any, Dict, List, Mapping, Optional, Pattern, Set, Tuple, Union
1919

2020
from matrix_common.regex import glob_to_regex, to_word_pattern
2121

@@ -120,11 +120,15 @@ def __init__(
120120
room_member_count: int,
121121
sender_power_level: int,
122122
power_levels: Dict[str, Union[int, Dict[str, int]]],
123+
relations: Dict[str, Set[Tuple[str, str]]],
124+
relations_match_enabled: bool,
123125
):
124126
self._event = event
125127
self._room_member_count = room_member_count
126128
self._sender_power_level = sender_power_level
127129
self._power_levels = power_levels
130+
self._relations = relations
131+
self._relations_match_enabled = relations_match_enabled
128132

129133
# Maps strings of e.g. 'content.body' -> event["content"]["body"]
130134
self._value_cache = _flatten_dict(event)
@@ -188,7 +192,16 @@ def matches(
188192
return _sender_notification_permission(
189193
self._event, condition, self._sender_power_level, self._power_levels
190194
)
195+
elif (
196+
condition["kind"] == "org.matrix.msc3772.relation_match"
197+
and self._relations_match_enabled
198+
):
199+
return self._relation_match(condition, user_id)
191200
else:
201+
# XXX This looks incorrect -- we have reached an unknown condition
202+
# kind and are unconditionally returning that it matches. Note
203+
# that it seems possible to provide a condition to the /pushrules
204+
# endpoint with an unknown kind, see _rule_tuple_from_request_object.
192205
return True
193206

194207
def _event_match(self, condition: dict, user_id: str) -> bool:
@@ -256,6 +269,41 @@ def _contains_display_name(self, display_name: Optional[str]) -> bool:
256269

257270
return bool(r.search(body))
258271

272+
def _relation_match(self, condition: dict, user_id: str) -> bool:
273+
"""
274+
Check an "relation_match" push rule condition.
275+
276+
Args:
277+
condition: The "event_match" push rule condition to match.
278+
user_id: The user's MXID.
279+
280+
Returns:
281+
True if the condition matches the event, False otherwise.
282+
"""
283+
rel_type = condition.get("rel_type")
284+
if not rel_type:
285+
logger.warning("relation_match condition missing rel_type")
286+
return False
287+
288+
sender_pattern = condition.get("sender")
289+
if sender_pattern is None:
290+
sender_type = condition.get("sender_type")
291+
if sender_type == "user_id":
292+
sender_pattern = user_id
293+
type_pattern = condition.get("type")
294+
295+
# If any other relations matches, return True.
296+
for sender, event_type in self._relations.get(rel_type, ()):
297+
if sender_pattern and not _glob_matches(sender_pattern, sender):
298+
continue
299+
if type_pattern and not _glob_matches(type_pattern, event_type):
300+
continue
301+
# All values must have matched.
302+
return True
303+
304+
# No relations matched.
305+
return False
306+
259307

260308
# Caches (string, is_glob, word_boundary) -> regex for push. See _glob_matches
261309
regex_cache: LruCache[Tuple[str, bool, bool], Pattern] = LruCache(

synapse/storage/databases/main/events.py

+9
Original file line numberDiff line numberDiff line change
@@ -1828,6 +1828,10 @@ def _handle_event_relations(
18281828
self.store.get_aggregation_groups_for_event.invalidate,
18291829
(relation.parent_id,),
18301830
)
1831+
txn.call_after(
1832+
self.store.get_mutual_event_relations_for_rel_type.invalidate,
1833+
(relation.parent_id,),
1834+
)
18311835

18321836
if relation.rel_type == RelationTypes.REPLACE:
18331837
txn.call_after(
@@ -2004,6 +2008,11 @@ def _handle_redact_relations(
20042008
self.store._invalidate_cache_and_stream(
20052009
txn, self.store.get_thread_participated, (redacted_relates_to,)
20062010
)
2011+
self.store._invalidate_cache_and_stream(
2012+
txn,
2013+
self.store.get_mutual_event_relations_for_rel_type,
2014+
(redacted_relates_to,),
2015+
)
20072016

20082017
self.db_pool.simple_delete_txn(
20092018
txn, table="event_relations", keyvalues={"event_id": redacted_event_id}

synapse/storage/databases/main/push_rule.py

+5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ def _is_experimental_rule_enabled(
6161
and not experimental_config.msc3786_enabled
6262
):
6363
return False
64+
if (
65+
rule_id == "global/underride/.org.matrix.msc3772.thread_reply"
66+
and not experimental_config.msc3772_enabled
67+
):
68+
return False
6469
return True
6570

6671

synapse/storage/databases/main/relations.py

+52
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import logging
16+
from collections import defaultdict
1617
from typing import (
1718
Collection,
1819
Dict,
@@ -767,6 +768,57 @@ def _get_if_user_has_annotated_event(txn: LoggingTransaction) -> bool:
767768
"get_if_user_has_annotated_event", _get_if_user_has_annotated_event
768769
)
769770

771+
@cached(iterable=True)
772+
async def get_mutual_event_relations_for_rel_type(
773+
self, event_id: str, relation_type: str
774+
) -> Set[Tuple[str, str]]:
775+
raise NotImplementedError()
776+
777+
@cachedList(
778+
cached_method_name="get_mutual_event_relations_for_rel_type",
779+
list_name="relation_types",
780+
)
781+
async def get_mutual_event_relations(
782+
self, event_id: str, relation_types: Collection[str]
783+
) -> Dict[str, Set[Tuple[str, str]]]:
784+
"""
785+
Fetch event metadata for events which related to the same event as the given event.
786+
787+
If the given event has no relation information, returns an empty dictionary.
788+
789+
Args:
790+
event_id: The event ID which is targeted by relations.
791+
relation_types: The relation types to check for mutual relations.
792+
793+
Returns:
794+
A dictionary of relation type to:
795+
A set of tuples of:
796+
The sender
797+
The event type
798+
"""
799+
rel_type_sql, rel_type_args = make_in_list_sql_clause(
800+
self.database_engine, "relation_type", relation_types
801+
)
802+
803+
sql = f"""
804+
SELECT DISTINCT relation_type, sender, type FROM event_relations
805+
INNER JOIN events USING (event_id)
806+
WHERE relates_to_id = ? AND {rel_type_sql}
807+
"""
808+
809+
def _get_event_relations(
810+
txn: LoggingTransaction,
811+
) -> Dict[str, Set[Tuple[str, str]]]:
812+
txn.execute(sql, [event_id] + rel_type_args)
813+
result = defaultdict(set)
814+
for rel_type, sender, type in txn.fetchall():
815+
result[rel_type].add((sender, type))
816+
return result
817+
818+
return await self.db_pool.runInteraction(
819+
"get_event_relations", _get_event_relations
820+
)
821+
770822

771823
class RelationsStore(RelationsWorkerStore):
772824
pass

0 commit comments

Comments
 (0)