Make push rules use proper structures. (#13522)

This improves load times for push rules:

| Version              | Time per user | Time for 1k users | 
| -------------------- | ------------- | ----------------- |
| Before               |       138 µs  |             138ms |
| Now (with custom)    |       2.11 µs |            2.11ms |
| Now (without custom) |       49.7 ns |           0.05 ms |

This therefore has a large impact on send times for rooms
with large numbers of local users in the room.
This commit is contained in:
Erik Johnston 2022-08-16 12:22:17 +01:00 committed by GitHub
parent d642ce4b32
commit 5442891cbc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 495 additions and 334 deletions

1
changelog.d/13522.misc Normal file
View file

@ -0,0 +1 @@
Improve performance of sending messages in rooms with thousands of local users.

View file

@ -14,128 +14,224 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import copy
from typing import Any, Dict, List
from synapse.push.rulekinds import PRIORITY_CLASS_INVERSE_MAP, PRIORITY_CLASS_MAP
def list_with_base_rules(rawrules: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Combine the list of rules set by the user with the default push rules
Args:
rawrules: The rules the user has modified or set.
Returns:
A new list with the rules set by the user combined with the defaults.
""" """
ruleslist = [] Push rules is the system used to determine which events trigger a push (and a
bump in notification counts).
# Grab the base rules that the user has modified. This consists of a list of "push rules" for each user, where a push rule is a
# The modified base rules have a priority_class of -1. pair of "conditions" and "actions". When a user receives an event Synapse
modified_base_rules = {r["rule_id"]: r for r in rawrules if r["priority_class"] < 0} iterates over the list of push rules until it finds one where all the conditions
match the event, at which point "actions" describe the outcome (e.g. notify,
highlight, etc).
# Remove the modified base rules from the list, They'll be added back Push rules are split up into 5 different "kinds" (aka "priority classes"), which
# in the default positions in the list. are run in order:
rawrules = [r for r in rawrules if r["priority_class"] >= 0] 1. Override highest priority rules, e.g. always ignore notices
2. Content content specific rules, e.g. @ notifications
3. Room per room rules, e.g. enable/disable notifications for all messages
in a room
4. Sender per sender rules, e.g. never notify for messages from a given
user
5. Underride the lowest priority "default" rules, e.g. notify for every
message.
# shove the server default rules for each kind onto the end of each The set of "base rules" are the list of rules that every user has by default. A
current_prio_class = list(PRIORITY_CLASS_INVERSE_MAP)[-1] user can modify their copy of the push rules in one of three ways:
ruleslist.extend( 1. Adding a new push rule of a certain kind
make_base_prepend_rules( 2. Changing the actions of a base rule
PRIORITY_CLASS_INVERSE_MAP[current_prio_class], modified_base_rules 3. Enabling/disabling a base rule.
)
The base rules are split into whether they come before or after a particular
kind, so the order of push rule evaluation would be: base rules for before
"override" kind, user defined "override" rules, base rules after "override"
kind, etc, etc.
"""
import itertools
from typing import Dict, Iterator, List, Mapping, Sequence, Tuple, Union
import attr
from synapse.config.experimental import ExperimentalConfig
from synapse.push.rulekinds import PRIORITY_CLASS_MAP
@attr.s(auto_attribs=True, slots=True, frozen=True)
class PushRule:
"""A push rule
Attributes:
rule_id: a unique ID for this rule
priority_class: what "kind" of push rule this is (see
`PRIORITY_CLASS_MAP` for mapping between int and kind)
conditions: the sequence of conditions that all need to match
actions: the actions to apply if all conditions are met
default: is this a base rule?
default_enabled: is this enabled by default?
"""
rule_id: str
priority_class: int
conditions: Sequence[Mapping[str, str]]
actions: Sequence[Union[str, Mapping]]
default: bool = False
default_enabled: bool = True
@attr.s(auto_attribs=True, slots=True, frozen=True, weakref_slot=False)
class PushRules:
"""A collection of push rules for an account.
Can be iterated over, producing push rules in priority order.
"""
# A mapping from rule ID to push rule that overrides a base rule. These will
# be returned instead of the base rule.
overriden_base_rules: Dict[str, PushRule] = attr.Factory(dict)
# The following stores the custom push rules at each priority class.
#
# We keep these separate (rather than combining into one big list) to avoid
# copying the base rules around all the time.
override: List[PushRule] = attr.Factory(list)
content: List[PushRule] = attr.Factory(list)
room: List[PushRule] = attr.Factory(list)
sender: List[PushRule] = attr.Factory(list)
underride: List[PushRule] = attr.Factory(list)
def __iter__(self) -> Iterator[PushRule]:
# When iterating over the push rules we need to return the base rules
# interspersed at the correct spots.
for rule in itertools.chain(
BASE_PREPEND_OVERRIDE_RULES,
self.override,
BASE_APPEND_OVERRIDE_RULES,
self.content,
BASE_APPEND_CONTENT_RULES,
self.room,
self.sender,
self.underride,
BASE_APPEND_UNDERRIDE_RULES,
):
# Check if a base rule has been overriden by a custom rule. If so
# return that instead.
override_rule = self.overriden_base_rules.get(rule.rule_id)
if override_rule:
yield override_rule
else:
yield rule
def __len__(self) -> int:
# The length is mostly used by caches to get a sense of "size" / amount
# of memory this object is using, so we only count the number of custom
# rules.
return (
len(self.overriden_base_rules)
+ len(self.override)
+ len(self.content)
+ len(self.room)
+ len(self.sender)
+ len(self.underride)
) )
for r in rawrules:
if r["priority_class"] < current_prio_class:
while r["priority_class"] < current_prio_class:
ruleslist.extend(
make_base_append_rules(
PRIORITY_CLASS_INVERSE_MAP[current_prio_class],
modified_base_rules,
)
)
current_prio_class -= 1
if current_prio_class > 0:
ruleslist.extend(
make_base_prepend_rules(
PRIORITY_CLASS_INVERSE_MAP[current_prio_class],
modified_base_rules,
)
)
ruleslist.append(r) @attr.s(auto_attribs=True, slots=True, frozen=True, weakref_slot=False)
class FilteredPushRules:
"""A wrapper around `PushRules` that filters out disabled experimental push
rules, and includes the "enabled" state for each rule when iterated over.
"""
while current_prio_class > 0: push_rules: PushRules
ruleslist.extend( enabled_map: Dict[str, bool]
make_base_append_rules( experimental_config: ExperimentalConfig
PRIORITY_CLASS_INVERSE_MAP[current_prio_class], modified_base_rules
)
)
current_prio_class -= 1
if current_prio_class > 0:
ruleslist.extend(
make_base_prepend_rules(
PRIORITY_CLASS_INVERSE_MAP[current_prio_class], modified_base_rules
)
)
return ruleslist def __iter__(self) -> Iterator[Tuple[PushRule, bool]]:
for rule in self.push_rules:
if not _is_experimental_rule_enabled(
rule.rule_id, self.experimental_config
):
continue
enabled = self.enabled_map.get(rule.rule_id, rule.default_enabled)
yield rule, enabled
def __len__(self) -> int:
return len(self.push_rules)
def make_base_append_rules( DEFAULT_EMPTY_PUSH_RULES = PushRules()
kind: str, modified_base_rules: Dict[str, Dict[str, Any]]
) -> List[Dict[str, Any]]:
rules = []
if kind == "override":
rules = BASE_APPEND_OVERRIDE_RULES
elif kind == "underride":
rules = BASE_APPEND_UNDERRIDE_RULES
elif kind == "content":
rules = BASE_APPEND_CONTENT_RULES
# Copy the rules before modifying them def compile_push_rules(rawrules: List[PushRule]) -> PushRules:
rules = copy.deepcopy(rules) """Given a set of custom push rules return a `PushRules` instance (which
for r in rules: includes the base rules).
# Only modify the actions, keep the conditions the same. """
assert isinstance(r["rule_id"], str)
modified = modified_base_rules.get(r["rule_id"]) if not rawrules:
if modified: # Fast path to avoid allocating empty lists when there are no custom
r["actions"] = modified["actions"] # rules for the user.
return DEFAULT_EMPTY_PUSH_RULES
rules = PushRules()
for rule in rawrules:
# We need to decide which bucket each custom push rule goes into.
# If it has the same ID as a base rule then it overrides that...
overriden_base_rule = BASE_RULES_BY_ID.get(rule.rule_id)
if overriden_base_rule:
rules.overriden_base_rules[rule.rule_id] = attr.evolve(
overriden_base_rule, actions=rule.actions
)
continue
# ... otherwise it gets added to the appropriate priority class bucket
collection: List[PushRule]
if rule.priority_class == 5:
collection = rules.override
elif rule.priority_class == 4:
collection = rules.content
elif rule.priority_class == 3:
collection = rules.room
elif rule.priority_class == 2:
collection = rules.sender
elif rule.priority_class == 1:
collection = rules.underride
else:
raise Exception(f"Unknown priority class: {rule.priority_class}")
collection.append(rule)
return rules return rules
def make_base_prepend_rules( def _is_experimental_rule_enabled(
kind: str, rule_id: str, experimental_config: ExperimentalConfig
modified_base_rules: Dict[str, Dict[str, Any]], ) -> bool:
) -> List[Dict[str, Any]]: """Used by `FilteredPushRules` to filter out experimental rules when they
rules = [] have not been enabled.
"""
if kind == "override": if (
rules = BASE_PREPEND_OVERRIDE_RULES rule_id == "global/override/.org.matrix.msc3786.rule.room.server_acl"
and not experimental_config.msc3786_enabled
# Copy the rules before modifying them ):
rules = copy.deepcopy(rules) return False
for r in rules: if (
# Only modify the actions, keep the conditions the same. rule_id == "global/underride/.org.matrix.msc3772.thread_reply"
assert isinstance(r["rule_id"], str) and not experimental_config.msc3772_enabled
modified = modified_base_rules.get(r["rule_id"]) ):
if modified: return False
r["actions"] = modified["actions"] return True
return rules
# We have to annotate these types, otherwise mypy infers them as BASE_APPEND_CONTENT_RULES = [
# `List[Dict[str, Sequence[Collection[str]]]]`. PushRule(
BASE_APPEND_CONTENT_RULES: List[Dict[str, Any]] = [ default=True,
{ priority_class=PRIORITY_CLASS_MAP["content"],
"rule_id": "global/content/.m.rule.contains_user_name", rule_id="global/content/.m.rule.contains_user_name",
"conditions": [ conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "content.body", "key": "content.body",
@ -143,29 +239,33 @@ BASE_APPEND_CONTENT_RULES: List[Dict[str, Any]] = [
"pattern_type": "user_localpart", "pattern_type": "user_localpart",
} }
], ],
"actions": [ actions=[
"notify", "notify",
{"set_tweak": "sound", "value": "default"}, {"set_tweak": "sound", "value": "default"},
{"set_tweak": "highlight"}, {"set_tweak": "highlight"},
], ],
} )
] ]
BASE_PREPEND_OVERRIDE_RULES: List[Dict[str, Any]] = [ BASE_PREPEND_OVERRIDE_RULES = [
{ PushRule(
"rule_id": "global/override/.m.rule.master", default=True,
"enabled": False, priority_class=PRIORITY_CLASS_MAP["override"],
"conditions": [], rule_id="global/override/.m.rule.master",
"actions": ["dont_notify"], default_enabled=False,
} conditions=[],
actions=["dont_notify"],
)
] ]
BASE_APPEND_OVERRIDE_RULES: List[Dict[str, Any]] = [ BASE_APPEND_OVERRIDE_RULES = [
{ PushRule(
"rule_id": "global/override/.m.rule.suppress_notices", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["override"],
rule_id="global/override/.m.rule.suppress_notices",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "content.msgtype", "key": "content.msgtype",
@ -173,13 +273,15 @@ BASE_APPEND_OVERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_suppress_notices", "_cache_key": "_suppress_notices",
} }
], ],
"actions": ["dont_notify"], actions=["dont_notify"],
}, ),
# NB. .m.rule.invite_for_me must be higher prio than .m.rule.member_event # NB. .m.rule.invite_for_me must be higher prio than .m.rule.member_event
# otherwise invites will be matched by .m.rule.member_event # otherwise invites will be matched by .m.rule.member_event
{ PushRule(
"rule_id": "global/override/.m.rule.invite_for_me", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["override"],
rule_id="global/override/.m.rule.invite_for_me",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "type", "key": "type",
@ -195,21 +297,23 @@ BASE_APPEND_OVERRIDE_RULES: List[Dict[str, Any]] = [
# Match the requester's MXID. # Match the requester's MXID.
{"kind": "event_match", "key": "state_key", "pattern_type": "user_id"}, {"kind": "event_match", "key": "state_key", "pattern_type": "user_id"},
], ],
"actions": [ actions=[
"notify", "notify",
{"set_tweak": "sound", "value": "default"}, {"set_tweak": "sound", "value": "default"},
{"set_tweak": "highlight", "value": False}, {"set_tweak": "highlight", "value": False},
], ],
}, ),
# Will we sometimes want to know about people joining and leaving? # Will we sometimes want to know about people joining and leaving?
# Perhaps: if so, this could be expanded upon. Seems the most usual case # Perhaps: if so, this could be expanded upon. Seems the most usual case
# is that we don't though. We add this override rule so that even if # is that we don't though. We add this override rule so that even if
# the room rule is set to notify, we don't get notifications about # the room rule is set to notify, we don't get notifications about
# join/leave/avatar/displayname events. # join/leave/avatar/displayname events.
# See also: https://matrix.org/jira/browse/SYN-607 # See also: https://matrix.org/jira/browse/SYN-607
{ PushRule(
"rule_id": "global/override/.m.rule.member_event", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["override"],
rule_id="global/override/.m.rule.member_event",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "type", "key": "type",
@ -217,24 +321,28 @@ BASE_APPEND_OVERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_member", "_cache_key": "_member",
} }
], ],
"actions": ["dont_notify"], actions=["dont_notify"],
}, ),
# This was changed from underride to override so it's closer in priority # This was changed from underride to override so it's closer in priority
# to the content rules where the user name highlight rule lives. This # to the content rules where the user name highlight rule lives. This
# way a room rule is lower priority than both but a custom override rule # way a room rule is lower priority than both but a custom override rule
# is higher priority than both. # is higher priority than both.
{ PushRule(
"rule_id": "global/override/.m.rule.contains_display_name", default=True,
"conditions": [{"kind": "contains_display_name"}], priority_class=PRIORITY_CLASS_MAP["override"],
"actions": [ rule_id="global/override/.m.rule.contains_display_name",
conditions=[{"kind": "contains_display_name"}],
actions=[
"notify", "notify",
{"set_tweak": "sound", "value": "default"}, {"set_tweak": "sound", "value": "default"},
{"set_tweak": "highlight"}, {"set_tweak": "highlight"},
], ],
}, ),
{ PushRule(
"rule_id": "global/override/.m.rule.roomnotif", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["override"],
rule_id="global/override/.m.rule.roomnotif",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "content.body", "key": "content.body",
@ -247,11 +355,13 @@ BASE_APPEND_OVERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_roomnotif_pl", "_cache_key": "_roomnotif_pl",
}, },
], ],
"actions": ["notify", {"set_tweak": "highlight", "value": True}], actions=["notify", {"set_tweak": "highlight", "value": True}],
}, ),
{ PushRule(
"rule_id": "global/override/.m.rule.tombstone", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["override"],
rule_id="global/override/.m.rule.tombstone",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "type", "key": "type",
@ -265,11 +375,13 @@ BASE_APPEND_OVERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_tombstone_statekey", "_cache_key": "_tombstone_statekey",
}, },
], ],
"actions": ["notify", {"set_tweak": "highlight", "value": True}], actions=["notify", {"set_tweak": "highlight", "value": True}],
}, ),
{ PushRule(
"rule_id": "global/override/.m.rule.reaction", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["override"],
rule_id="global/override/.m.rule.reaction",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "type", "key": "type",
@ -277,14 +389,16 @@ BASE_APPEND_OVERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_reaction", "_cache_key": "_reaction",
} }
], ],
"actions": ["dont_notify"], actions=["dont_notify"],
}, ),
# XXX: This is an experimental rule that is only enabled if msc3786_enabled # XXX: This is an experimental rule that is only enabled if msc3786_enabled
# is enabled, if it is not the rule gets filtered out in _load_rules() in # is enabled, if it is not the rule gets filtered out in _load_rules() in
# PushRulesWorkerStore # PushRulesWorkerStore
{ PushRule(
"rule_id": "global/override/.org.matrix.msc3786.rule.room.server_acl", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["override"],
rule_id="global/override/.org.matrix.msc3786.rule.room.server_acl",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "type", "key": "type",
@ -298,15 +412,17 @@ BASE_APPEND_OVERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_room_server_acl_state_key", "_cache_key": "_room_server_acl_state_key",
}, },
], ],
"actions": [], actions=[],
}, ),
] ]
BASE_APPEND_UNDERRIDE_RULES: List[Dict[str, Any]] = [ BASE_APPEND_UNDERRIDE_RULES = [
{ PushRule(
"rule_id": "global/underride/.m.rule.call", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["underride"],
rule_id="global/underride/.m.rule.call",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "type", "key": "type",
@ -314,17 +430,19 @@ BASE_APPEND_UNDERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_call", "_cache_key": "_call",
} }
], ],
"actions": [ actions=[
"notify", "notify",
{"set_tweak": "sound", "value": "ring"}, {"set_tweak": "sound", "value": "ring"},
{"set_tweak": "highlight", "value": False}, {"set_tweak": "highlight", "value": False},
], ],
}, ),
# XXX: once m.direct is standardised everywhere, we should use it to detect # XXX: once m.direct is standardised everywhere, we should use it to detect
# a DM from the user's perspective rather than this heuristic. # a DM from the user's perspective rather than this heuristic.
{ PushRule(
"rule_id": "global/underride/.m.rule.room_one_to_one", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["underride"],
rule_id="global/underride/.m.rule.room_one_to_one",
conditions=[
{"kind": "room_member_count", "is": "2", "_cache_key": "member_count"}, {"kind": "room_member_count", "is": "2", "_cache_key": "member_count"},
{ {
"kind": "event_match", "kind": "event_match",
@ -333,17 +451,19 @@ BASE_APPEND_UNDERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_message", "_cache_key": "_message",
}, },
], ],
"actions": [ actions=[
"notify", "notify",
{"set_tweak": "sound", "value": "default"}, {"set_tweak": "sound", "value": "default"},
{"set_tweak": "highlight", "value": False}, {"set_tweak": "highlight", "value": False},
], ],
}, ),
# XXX: this is going to fire for events which aren't m.room.messages # XXX: this is going to fire for events which aren't m.room.messages
# but are encrypted (e.g. m.call.*)... # but are encrypted (e.g. m.call.*)...
{ PushRule(
"rule_id": "global/underride/.m.rule.encrypted_room_one_to_one", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["underride"],
rule_id="global/underride/.m.rule.encrypted_room_one_to_one",
conditions=[
{"kind": "room_member_count", "is": "2", "_cache_key": "member_count"}, {"kind": "room_member_count", "is": "2", "_cache_key": "member_count"},
{ {
"kind": "event_match", "kind": "event_match",
@ -352,15 +472,17 @@ BASE_APPEND_UNDERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_encrypted", "_cache_key": "_encrypted",
}, },
], ],
"actions": [ actions=[
"notify", "notify",
{"set_tweak": "sound", "value": "default"}, {"set_tweak": "sound", "value": "default"},
{"set_tweak": "highlight", "value": False}, {"set_tweak": "highlight", "value": False},
], ],
}, ),
{ PushRule(
"rule_id": "global/underride/.org.matrix.msc3772.thread_reply", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["underride"],
rule_id="global/underride/.org.matrix.msc3772.thread_reply",
conditions=[
{ {
"kind": "org.matrix.msc3772.relation_match", "kind": "org.matrix.msc3772.relation_match",
"rel_type": "m.thread", "rel_type": "m.thread",
@ -368,11 +490,13 @@ BASE_APPEND_UNDERRIDE_RULES: List[Dict[str, Any]] = [
"sender_type": "user_id", "sender_type": "user_id",
} }
], ],
"actions": ["notify", {"set_tweak": "highlight", "value": False}], actions=["notify", {"set_tweak": "highlight", "value": False}],
}, ),
{ PushRule(
"rule_id": "global/underride/.m.rule.message", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["underride"],
rule_id="global/underride/.m.rule.message",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "type", "key": "type",
@ -380,13 +504,15 @@ BASE_APPEND_UNDERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_message", "_cache_key": "_message",
} }
], ],
"actions": ["notify", {"set_tweak": "highlight", "value": False}], actions=["notify", {"set_tweak": "highlight", "value": False}],
}, ),
# XXX: this is going to fire for events which aren't m.room.messages # XXX: this is going to fire for events which aren't m.room.messages
# but are encrypted (e.g. m.call.*)... # but are encrypted (e.g. m.call.*)...
{ PushRule(
"rule_id": "global/underride/.m.rule.encrypted", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["underride"],
rule_id="global/underride/.m.rule.encrypted",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "type", "key": "type",
@ -394,11 +520,13 @@ BASE_APPEND_UNDERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_encrypted", "_cache_key": "_encrypted",
} }
], ],
"actions": ["notify", {"set_tweak": "highlight", "value": False}], actions=["notify", {"set_tweak": "highlight", "value": False}],
}, ),
{ PushRule(
"rule_id": "global/underride/.im.vector.jitsi", default=True,
"conditions": [ priority_class=PRIORITY_CLASS_MAP["underride"],
rule_id="global/underride/.im.vector.jitsi",
conditions=[
{ {
"kind": "event_match", "kind": "event_match",
"key": "type", "key": "type",
@ -418,29 +546,27 @@ BASE_APPEND_UNDERRIDE_RULES: List[Dict[str, Any]] = [
"_cache_key": "_is_state_event", "_cache_key": "_is_state_event",
}, },
], ],
"actions": ["notify", {"set_tweak": "highlight", "value": False}], actions=["notify", {"set_tweak": "highlight", "value": False}],
}, ),
] ]
BASE_RULE_IDS = set() BASE_RULE_IDS = set()
BASE_RULES_BY_ID: Dict[str, PushRule] = {}
for r in BASE_APPEND_CONTENT_RULES: for r in BASE_APPEND_CONTENT_RULES:
r["priority_class"] = PRIORITY_CLASS_MAP["content"] BASE_RULE_IDS.add(r.rule_id)
r["default"] = True BASE_RULES_BY_ID[r.rule_id] = r
BASE_RULE_IDS.add(r["rule_id"])
for r in BASE_PREPEND_OVERRIDE_RULES: for r in BASE_PREPEND_OVERRIDE_RULES:
r["priority_class"] = PRIORITY_CLASS_MAP["override"] BASE_RULE_IDS.add(r.rule_id)
r["default"] = True BASE_RULES_BY_ID[r.rule_id] = r
BASE_RULE_IDS.add(r["rule_id"])
for r in BASE_APPEND_OVERRIDE_RULES: for r in BASE_APPEND_OVERRIDE_RULES:
r["priority_class"] = PRIORITY_CLASS_MAP["override"] BASE_RULE_IDS.add(r.rule_id)
r["default"] = True BASE_RULES_BY_ID[r.rule_id] = r
BASE_RULE_IDS.add(r["rule_id"])
for r in BASE_APPEND_UNDERRIDE_RULES: for r in BASE_APPEND_UNDERRIDE_RULES:
r["priority_class"] = PRIORITY_CLASS_MAP["underride"] BASE_RULE_IDS.add(r.rule_id)
r["default"] = True BASE_RULES_BY_ID[r.rule_id] = r
BASE_RULE_IDS.add(r["rule_id"])

View file

@ -15,7 +15,18 @@
import itertools import itertools
import logging import logging
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Set, Tuple, Union from typing import (
TYPE_CHECKING,
Collection,
Dict,
Iterable,
List,
Mapping,
Optional,
Set,
Tuple,
Union,
)
from prometheus_client import Counter from prometheus_client import Counter
@ -30,6 +41,7 @@ from synapse.util.caches import register_cache
from synapse.util.metrics import measure_func from synapse.util.metrics import measure_func
from synapse.visibility import filter_event_for_clients_with_state from synapse.visibility import filter_event_for_clients_with_state
from .baserules import FilteredPushRules, PushRule
from .push_rule_evaluator import PushRuleEvaluatorForEvent from .push_rule_evaluator import PushRuleEvaluatorForEvent
if TYPE_CHECKING: if TYPE_CHECKING:
@ -112,7 +124,7 @@ class BulkPushRuleEvaluator:
async def _get_rules_for_event( async def _get_rules_for_event(
self, self,
event: EventBase, event: EventBase,
) -> Dict[str, List[Dict[str, Any]]]: ) -> Dict[str, FilteredPushRules]:
"""Get the push rules for all users who may need to be notified about """Get the push rules for all users who may need to be notified about
the event. the event.
@ -186,7 +198,7 @@ class BulkPushRuleEvaluator:
return pl_event.content if pl_event else {}, sender_level return pl_event.content if pl_event else {}, sender_level
async def _get_mutual_relations( async def _get_mutual_relations(
self, event: EventBase, rules: Iterable[Dict[str, Any]] self, event: EventBase, rules: Iterable[Tuple[PushRule, bool]]
) -> Dict[str, Set[Tuple[str, str]]]: ) -> Dict[str, Set[Tuple[str, str]]]:
""" """
Fetch event metadata for events which related to the same event as the given event. Fetch event metadata for events which related to the same event as the given event.
@ -216,12 +228,11 @@ class BulkPushRuleEvaluator:
# Pre-filter to figure out which relation types are interesting. # Pre-filter to figure out which relation types are interesting.
rel_types = set() rel_types = set()
for rule in rules: for rule, enabled in rules:
# Skip disabled rules. if not enabled:
if "enabled" in rule and not rule["enabled"]:
continue continue
for condition in rule["conditions"]: for condition in rule.conditions:
if condition["kind"] != "org.matrix.msc3772.relation_match": if condition["kind"] != "org.matrix.msc3772.relation_match":
continue continue
@ -254,7 +265,7 @@ class BulkPushRuleEvaluator:
count_as_unread = _should_count_as_unread(event, context) count_as_unread = _should_count_as_unread(event, context)
rules_by_user = await self._get_rules_for_event(event) rules_by_user = await self._get_rules_for_event(event)
actions_by_user: Dict[str, List[Union[dict, str]]] = {} actions_by_user: Dict[str, Collection[Union[Mapping, str]]] = {}
room_member_count = await self.store.get_number_joined_users_in_room( room_member_count = await self.store.get_number_joined_users_in_room(
event.room_id event.room_id
@ -317,15 +328,13 @@ class BulkPushRuleEvaluator:
# current user, it'll be added to the dict later. # current user, it'll be added to the dict later.
actions_by_user[uid] = [] actions_by_user[uid] = []
for rule in rules: for rule, enabled in rules:
if "enabled" in rule and not rule["enabled"]: if not enabled:
continue continue
matches = evaluator.check_conditions( matches = evaluator.check_conditions(rule.conditions, uid, display_name)
rule["conditions"], uid, display_name
)
if matches: if matches:
actions = [x for x in rule["actions"] if x != "dont_notify"] actions = [x for x in rule.actions if x != "dont_notify"]
if actions and "notify" in actions: if actions and "notify" in actions:
# Push rules say we should notify the user of this event # Push rules say we should notify the user of this event
actions_by_user[uid] = actions actions_by_user[uid] = actions

View file

@ -18,16 +18,15 @@ from typing import Any, Dict, List, Optional
from synapse.push.rulekinds import PRIORITY_CLASS_INVERSE_MAP, PRIORITY_CLASS_MAP from synapse.push.rulekinds import PRIORITY_CLASS_INVERSE_MAP, PRIORITY_CLASS_MAP
from synapse.types import UserID from synapse.types import UserID
from .baserules import FilteredPushRules, PushRule
def format_push_rules_for_user( def format_push_rules_for_user(
user: UserID, ruleslist: List user: UserID, ruleslist: FilteredPushRules
) -> Dict[str, Dict[str, list]]: ) -> Dict[str, Dict[str, list]]:
"""Converts a list of rawrules and a enabled map into nested dictionaries """Converts a list of rawrules and a enabled map into nested dictionaries
to match the Matrix client-server format for push rules""" to match the Matrix client-server format for push rules"""
# We're going to be mutating this a lot, so do a deep copy
ruleslist = copy.deepcopy(ruleslist)
rules: Dict[str, Dict[str, List[Dict[str, Any]]]] = { rules: Dict[str, Dict[str, List[Dict[str, Any]]]] = {
"global": {}, "global": {},
"device": {}, "device": {},
@ -35,11 +34,30 @@ def format_push_rules_for_user(
rules["global"] = _add_empty_priority_class_arrays(rules["global"]) rules["global"] = _add_empty_priority_class_arrays(rules["global"])
for r in ruleslist: for r, enabled in ruleslist:
template_name = _priority_class_to_template_name(r["priority_class"]) template_name = _priority_class_to_template_name(r.priority_class)
rulearray = rules["global"][template_name]
template_rule = _rule_to_template(r)
if not template_rule:
continue
rulearray.append(template_rule)
template_rule["enabled"] = enabled
if "conditions" not in template_rule:
# Not all formatted rules have explicit conditions, e.g. "room"
# rules omit them as they can be derived from the kind and rule ID.
#
# If the formatted rule has no conditions then we can skip the
# formatting of conditions.
continue
# Remove internal stuff. # Remove internal stuff.
for c in r["conditions"]: template_rule["conditions"] = copy.deepcopy(template_rule["conditions"])
for c in template_rule["conditions"]:
c.pop("_cache_key", None) c.pop("_cache_key", None)
pattern_type = c.pop("pattern_type", None) pattern_type = c.pop("pattern_type", None)
@ -52,16 +70,6 @@ def format_push_rules_for_user(
if sender_type == "user_id": if sender_type == "user_id":
c["sender"] = user.to_string() c["sender"] = user.to_string()
rulearray = rules["global"][template_name]
template_rule = _rule_to_template(r)
if template_rule:
if "enabled" in r:
template_rule["enabled"] = r["enabled"]
else:
template_rule["enabled"] = True
rulearray.append(template_rule)
return rules return rules
@ -71,24 +79,24 @@ def _add_empty_priority_class_arrays(d: Dict[str, list]) -> Dict[str, list]:
return d return d
def _rule_to_template(rule: Dict[str, Any]) -> Optional[Dict[str, Any]]: def _rule_to_template(rule: PushRule) -> Optional[Dict[str, Any]]:
unscoped_rule_id = None templaterule: Dict[str, Any]
if "rule_id" in rule:
unscoped_rule_id = _rule_id_from_namespaced(rule["rule_id"])
template_name = _priority_class_to_template_name(rule["priority_class"]) unscoped_rule_id = _rule_id_from_namespaced(rule.rule_id)
template_name = _priority_class_to_template_name(rule.priority_class)
if template_name in ["override", "underride"]: if template_name in ["override", "underride"]:
templaterule = {k: rule[k] for k in ["conditions", "actions"]} templaterule = {"conditions": rule.conditions, "actions": rule.actions}
elif template_name in ["sender", "room"]: elif template_name in ["sender", "room"]:
templaterule = {"actions": rule["actions"]} templaterule = {"actions": rule.actions}
unscoped_rule_id = rule["conditions"][0]["pattern"] unscoped_rule_id = rule.conditions[0]["pattern"]
elif template_name == "content": elif template_name == "content":
if len(rule["conditions"]) != 1: if len(rule.conditions) != 1:
return None return None
thecond = rule["conditions"][0] thecond = rule.conditions[0]
if "pattern" not in thecond: if "pattern" not in thecond:
return None return None
templaterule = {"actions": rule["actions"]} templaterule = {"actions": rule.actions}
templaterule["pattern"] = thecond["pattern"] templaterule["pattern"] = thecond["pattern"]
else: else:
# This should not be reached unless this function is not kept in sync # This should not be reached unless this function is not kept in sync
@ -97,8 +105,8 @@ def _rule_to_template(rule: Dict[str, Any]) -> Optional[Dict[str, Any]]:
if unscoped_rule_id: if unscoped_rule_id:
templaterule["rule_id"] = unscoped_rule_id templaterule["rule_id"] = unscoped_rule_id
if "default" in rule: if rule.default:
templaterule["default"] = rule["default"] templaterule["default"] = True
return templaterule return templaterule

View file

@ -15,7 +15,18 @@
import logging import logging
import re import re
from typing import Any, Dict, List, Mapping, Optional, Pattern, Set, Tuple, Union from typing import (
Any,
Dict,
List,
Mapping,
Optional,
Pattern,
Sequence,
Set,
Tuple,
Union,
)
from matrix_common.regex import glob_to_regex, to_word_pattern from matrix_common.regex import glob_to_regex, to_word_pattern
@ -32,14 +43,14 @@ INEQUALITY_EXPR = re.compile("^([=<>]*)([0-9]*)$")
def _room_member_count( def _room_member_count(
ev: EventBase, condition: Dict[str, Any], room_member_count: int ev: EventBase, condition: Mapping[str, Any], room_member_count: int
) -> bool: ) -> bool:
return _test_ineq_condition(condition, room_member_count) return _test_ineq_condition(condition, room_member_count)
def _sender_notification_permission( def _sender_notification_permission(
ev: EventBase, ev: EventBase,
condition: Dict[str, Any], condition: Mapping[str, Any],
sender_power_level: int, sender_power_level: int,
power_levels: Dict[str, Union[int, Dict[str, int]]], power_levels: Dict[str, Union[int, Dict[str, int]]],
) -> bool: ) -> bool:
@ -54,7 +65,7 @@ def _sender_notification_permission(
return sender_power_level >= room_notif_level return sender_power_level >= room_notif_level
def _test_ineq_condition(condition: Dict[str, Any], number: int) -> bool: def _test_ineq_condition(condition: Mapping[str, Any], number: int) -> bool:
if "is" not in condition: if "is" not in condition:
return False return False
m = INEQUALITY_EXPR.match(condition["is"]) m = INEQUALITY_EXPR.match(condition["is"])
@ -137,7 +148,7 @@ class PushRuleEvaluatorForEvent:
self._condition_cache: Dict[str, bool] = {} self._condition_cache: Dict[str, bool] = {}
def check_conditions( def check_conditions(
self, conditions: List[dict], uid: str, display_name: Optional[str] self, conditions: Sequence[Mapping], uid: str, display_name: Optional[str]
) -> bool: ) -> bool:
""" """
Returns true if a user's conditions/user ID/display name match the event. Returns true if a user's conditions/user ID/display name match the event.
@ -169,7 +180,7 @@ class PushRuleEvaluatorForEvent:
return True return True
def matches( def matches(
self, condition: Dict[str, Any], user_id: str, display_name: Optional[str] self, condition: Mapping[str, Any], user_id: str, display_name: Optional[str]
) -> bool: ) -> bool:
""" """
Returns true if a user's condition/user ID/display name match the event. Returns true if a user's condition/user ID/display name match the event.
@ -204,7 +215,7 @@ class PushRuleEvaluatorForEvent:
# endpoint with an unknown kind, see _rule_tuple_from_request_object. # endpoint with an unknown kind, see _rule_tuple_from_request_object.
return True return True
def _event_match(self, condition: dict, user_id: str) -> bool: def _event_match(self, condition: Mapping, user_id: str) -> bool:
""" """
Check an "event_match" push rule condition. Check an "event_match" push rule condition.
@ -269,7 +280,7 @@ class PushRuleEvaluatorForEvent:
return bool(r.search(body)) return bool(r.search(body))
def _relation_match(self, condition: dict, user_id: str) -> bool: def _relation_match(self, condition: Mapping, user_id: str) -> bool:
""" """
Check an "relation_match" push rule condition. Check an "relation_match" push rule condition.

View file

@ -74,7 +74,17 @@ receipt.
""" """
import logging import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union, cast from typing import (
TYPE_CHECKING,
Collection,
Dict,
List,
Mapping,
Optional,
Tuple,
Union,
cast,
)
import attr import attr
@ -154,7 +164,9 @@ class NotifCounts:
highlight_count: int = 0 highlight_count: int = 0
def _serialize_action(actions: List[Union[dict, str]], is_highlight: bool) -> str: def _serialize_action(
actions: Collection[Union[Mapping, str]], is_highlight: bool
) -> str:
"""Custom serializer for actions. This allows us to "compress" common actions. """Custom serializer for actions. This allows us to "compress" common actions.
We use the fact that most users have the same actions for notifs (and for We use the fact that most users have the same actions for notifs (and for
@ -750,7 +762,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
async def add_push_actions_to_staging( async def add_push_actions_to_staging(
self, self,
event_id: str, event_id: str,
user_id_actions: Dict[str, List[Union[dict, str]]], user_id_actions: Dict[str, Collection[Union[Mapping, str]]],
count_as_unread: bool, count_as_unread: bool,
) -> None: ) -> None:
"""Add the push actions for the event to the push action staging area. """Add the push actions for the event to the push action staging area.
@ -767,7 +779,7 @@ class EventPushActionsWorkerStore(ReceiptsWorkerStore, StreamWorkerStore, SQLBas
# This is a helper function for generating the necessary tuple that # This is a helper function for generating the necessary tuple that
# can be used to insert into the `event_push_actions_staging` table. # can be used to insert into the `event_push_actions_staging` table.
def _gen_entry( def _gen_entry(
user_id: str, actions: List[Union[dict, str]] user_id: str, actions: Collection[Union[Mapping, str]]
) -> Tuple[str, str, str, int, int, int]: ) -> Tuple[str, str, str, int, int, int]:
is_highlight = 1 if _action_has_highlight(actions) else 0 is_highlight = 1 if _action_has_highlight(actions) else 0
notif = 1 if "notify" in actions else 0 notif = 1 if "notify" in actions else 0
@ -1410,7 +1422,7 @@ class EventPushActionsStore(EventPushActionsWorkerStore):
] ]
def _action_has_highlight(actions: List[Union[dict, str]]) -> bool: def _action_has_highlight(actions: Collection[Union[Mapping, str]]) -> bool:
for action in actions: for action in actions:
if not isinstance(action, dict): if not isinstance(action, dict):
continue continue

View file

@ -14,11 +14,23 @@
# limitations under the License. # limitations under the License.
import abc import abc
import logging import logging
from typing import TYPE_CHECKING, Collection, Dict, List, Optional, Tuple, Union, cast from typing import (
TYPE_CHECKING,
Any,
Collection,
Dict,
List,
Mapping,
Optional,
Sequence,
Tuple,
Union,
cast,
)
from synapse.api.errors import StoreError from synapse.api.errors import StoreError
from synapse.config.homeserver import ExperimentalConfig from synapse.config.homeserver import ExperimentalConfig
from synapse.push.baserules import list_with_base_rules from synapse.push.baserules import FilteredPushRules, PushRule, compile_push_rules
from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker from synapse.replication.slave.storage._slaved_id_tracker import SlavedIdTracker
from synapse.storage._base import SQLBaseStore, db_to_json from synapse.storage._base import SQLBaseStore, db_to_json
from synapse.storage.database import ( from synapse.storage.database import (
@ -50,60 +62,30 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _is_experimental_rule_enabled(
rule_id: str, experimental_config: ExperimentalConfig
) -> bool:
"""Used by `_load_rules` to filter out experimental rules when they
have not been enabled.
"""
if (
rule_id == "global/override/.org.matrix.msc3786.rule.room.server_acl"
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
def _load_rules( def _load_rules(
rawrules: List[JsonDict], rawrules: List[JsonDict],
enabled_map: Dict[str, bool], enabled_map: Dict[str, bool],
experimental_config: ExperimentalConfig, experimental_config: ExperimentalConfig,
) -> List[JsonDict]: ) -> FilteredPushRules:
ruleslist = [] """Take the DB rows returned from the DB and convert them into a full
for rawrule in rawrules: `FilteredPushRules` object.
rule = dict(rawrule) """
rule["conditions"] = db_to_json(rawrule["conditions"])
rule["actions"] = db_to_json(rawrule["actions"])
rule["default"] = False
ruleslist.append(rule)
# We're going to be mutating this a lot, so copy it. We also filter out ruleslist = [
# any experimental default push rules that aren't enabled. PushRule(
rules = [ rule_id=rawrule["rule_id"],
rule priority_class=rawrule["priority_class"],
for rule in list_with_base_rules(ruleslist) conditions=db_to_json(rawrule["conditions"]),
if _is_experimental_rule_enabled(rule["rule_id"], experimental_config) actions=db_to_json(rawrule["actions"]),
)
for rawrule in rawrules
] ]
for i, rule in enumerate(rules): push_rules = compile_push_rules(ruleslist)
rule_id = rule["rule_id"]
if rule_id not in enabled_map: filtered_rules = FilteredPushRules(push_rules, enabled_map, experimental_config)
continue
if rule.get("enabled", True) == bool(enabled_map[rule_id]):
continue
# Rules are cached across users. return filtered_rules
rule = dict(rule)
rule["enabled"] = bool(enabled_map[rule_id])
rules[i] = rule
return rules
# The ABCMeta metaclass ensures that it cannot be instantiated without # The ABCMeta metaclass ensures that it cannot be instantiated without
@ -162,7 +144,7 @@ class PushRulesWorkerStore(
raise NotImplementedError() raise NotImplementedError()
@cached(max_entries=5000) @cached(max_entries=5000)
async def get_push_rules_for_user(self, user_id: str) -> List[JsonDict]: async def get_push_rules_for_user(self, user_id: str) -> FilteredPushRules:
rows = await self.db_pool.simple_select_list( rows = await self.db_pool.simple_select_list(
table="push_rules", table="push_rules",
keyvalues={"user_name": user_id}, keyvalues={"user_name": user_id},
@ -216,11 +198,11 @@ class PushRulesWorkerStore(
@cachedList(cached_method_name="get_push_rules_for_user", list_name="user_ids") @cachedList(cached_method_name="get_push_rules_for_user", list_name="user_ids")
async def bulk_get_push_rules( async def bulk_get_push_rules(
self, user_ids: Collection[str] self, user_ids: Collection[str]
) -> Dict[str, List[JsonDict]]: ) -> Dict[str, FilteredPushRules]:
if not user_ids: if not user_ids:
return {} return {}
results: Dict[str, List[JsonDict]] = {user_id: [] for user_id in user_ids} raw_rules: Dict[str, List[JsonDict]] = {user_id: [] for user_id in user_ids}
rows = await self.db_pool.simple_select_many_batch( rows = await self.db_pool.simple_select_many_batch(
table="push_rules", table="push_rules",
@ -234,11 +216,13 @@ class PushRulesWorkerStore(
rows.sort(key=lambda row: (-int(row["priority_class"]), -int(row["priority"]))) rows.sort(key=lambda row: (-int(row["priority_class"]), -int(row["priority"])))
for row in rows: for row in rows:
results.setdefault(row["user_name"], []).append(row) raw_rules.setdefault(row["user_name"], []).append(row)
enabled_map_by_user = await self.bulk_get_push_rules_enabled(user_ids) enabled_map_by_user = await self.bulk_get_push_rules_enabled(user_ids)
for user_id, rules in results.items(): results: Dict[str, FilteredPushRules] = {}
for user_id, rules in raw_rules.items():
results[user_id] = _load_rules( results[user_id] = _load_rules(
rules, enabled_map_by_user.get(user_id, {}), self.hs.config.experimental rules, enabled_map_by_user.get(user_id, {}), self.hs.config.experimental
) )
@ -345,8 +329,8 @@ class PushRuleStore(PushRulesWorkerStore):
user_id: str, user_id: str,
rule_id: str, rule_id: str,
priority_class: int, priority_class: int,
conditions: List[Dict[str, str]], conditions: Sequence[Mapping[str, str]],
actions: List[Union[JsonDict, str]], actions: Sequence[Union[Mapping[str, Any], str]],
before: Optional[str] = None, before: Optional[str] = None,
after: Optional[str] = None, after: Optional[str] = None,
) -> None: ) -> None:
@ -817,7 +801,7 @@ class PushRuleStore(PushRulesWorkerStore):
return self._push_rules_stream_id_gen.get_current_token() return self._push_rules_stream_id_gen.get_current_token()
async def copy_push_rule_from_room_to_room( async def copy_push_rule_from_room_to_room(
self, new_room_id: str, user_id: str, rule: dict self, new_room_id: str, user_id: str, rule: PushRule
) -> None: ) -> None:
"""Copy a single push rule from one room to another for a specific user. """Copy a single push rule from one room to another for a specific user.
@ -827,21 +811,27 @@ class PushRuleStore(PushRulesWorkerStore):
rule: A push rule. rule: A push rule.
""" """
# Create new rule id # Create new rule id
rule_id_scope = "/".join(rule["rule_id"].split("/")[:-1]) rule_id_scope = "/".join(rule.rule_id.split("/")[:-1])
new_rule_id = rule_id_scope + "/" + new_room_id new_rule_id = rule_id_scope + "/" + new_room_id
new_conditions = []
# Change room id in each condition # Change room id in each condition
for condition in rule.get("conditions", []): for condition in rule.conditions:
new_condition = condition
if condition.get("key") == "room_id": if condition.get("key") == "room_id":
condition["pattern"] = new_room_id new_condition = dict(condition)
new_condition["pattern"] = new_room_id
new_conditions.append(new_condition)
# Add the rule for the new room # Add the rule for the new room
await self.add_push_rule( await self.add_push_rule(
user_id=user_id, user_id=user_id,
rule_id=new_rule_id, rule_id=new_rule_id,
priority_class=rule["priority_class"], priority_class=rule.priority_class,
conditions=rule["conditions"], conditions=new_conditions,
actions=rule["actions"], actions=rule.actions,
) )
async def copy_push_rules_from_room_to_room_for_user( async def copy_push_rules_from_room_to_room_for_user(
@ -859,8 +849,11 @@ class PushRuleStore(PushRulesWorkerStore):
user_push_rules = await self.get_push_rules_for_user(user_id) user_push_rules = await self.get_push_rules_for_user(user_id)
# Get rules relating to the old room and copy them to the new room # Get rules relating to the old room and copy them to the new room
for rule in user_push_rules: for rule, enabled in user_push_rules:
conditions = rule.get("conditions", []) if not enabled:
continue
conditions = rule.conditions
if any( if any(
(c.get("key") == "room_id" and c.get("pattern") == old_room_id) (c.get("key") == "room_id" and c.get("pattern") == old_room_id)
for c in conditions for c in conditions

View file

@ -11,11 +11,11 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import Any, Dict
from twisted.test.proto_helpers import MemoryReactor from twisted.test.proto_helpers import MemoryReactor
from synapse.api.constants import AccountDataTypes from synapse.api.constants import AccountDataTypes
from synapse.push.baserules import PushRule
from synapse.push.rulekinds import PRIORITY_CLASS_MAP from synapse.push.rulekinds import PRIORITY_CLASS_MAP
from synapse.rest import admin from synapse.rest import admin
from synapse.rest.client import account, login from synapse.rest.client import account, login
@ -130,12 +130,12 @@ class DeactivateAccountTestCase(HomeserverTestCase):
), ),
) )
def _is_custom_rule(self, push_rule: Dict[str, Any]) -> bool: def _is_custom_rule(self, push_rule: PushRule) -> bool:
""" """
Default rules start with a dot: such as .m.rule and .im.vector. Default rules start with a dot: such as .m.rule and .im.vector.
This function returns true iff a rule is custom (not default). This function returns true iff a rule is custom (not default).
""" """
return "/." not in push_rule["rule_id"] return "/." not in push_rule.rule_id
def test_push_rules_deleted_upon_account_deactivation(self) -> None: def test_push_rules_deleted_upon_account_deactivation(self) -> None:
""" """
@ -157,22 +157,21 @@ class DeactivateAccountTestCase(HomeserverTestCase):
) )
# Test the rule exists # Test the rule exists
push_rules = self.get_success(self._store.get_push_rules_for_user(self.user)) filtered_push_rules = self.get_success(
self._store.get_push_rules_for_user(self.user)
)
# Filter out default rules; we don't care # Filter out default rules; we don't care
push_rules = list(filter(self._is_custom_rule, push_rules)) push_rules = [r for r, _ in filtered_push_rules if self._is_custom_rule(r)]
# Check our rule made it # Check our rule made it
self.assertEqual( self.assertEqual(
push_rules, push_rules,
[ [
{ PushRule(
"user_name": "@user:test", rule_id="personal.override.rule1",
"rule_id": "personal.override.rule1", priority_class=5,
"priority_class": 5, conditions=[],
"priority": 0, actions=[],
"conditions": [], )
"actions": [],
"default": False,
}
], ],
push_rules, push_rules,
) )
@ -180,9 +179,11 @@ class DeactivateAccountTestCase(HomeserverTestCase):
# Request the deactivation of our account # Request the deactivation of our account
self._deactivate_my_account() self._deactivate_my_account()
push_rules = self.get_success(self._store.get_push_rules_for_user(self.user)) filtered_push_rules = self.get_success(
self._store.get_push_rules_for_user(self.user)
)
# Filter out default rules; we don't care # Filter out default rules; we don't care
push_rules = list(filter(self._is_custom_rule, push_rules)) push_rules = [r for r, _ in filtered_push_rules if self._is_custom_rule(r)]
# Check our rule no longer exists # Check our rule no longer exists
self.assertEqual(push_rules, [], push_rules) self.assertEqual(push_rules, [], push_rules)