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

Add a module API to allow modules to edit push rule actions #12406

Merged
merged 14 commits into from
Apr 27, 2022
1 change: 1 addition & 0 deletions changelog.d/12406.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a module API to allow modules to create new push rules for local users.
94 changes: 94 additions & 0 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@
)
from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.rest.client.login import LoginResponse
from synapse.rest.client.push_rule import (
InvalidRuleException,
RuleSpec,
_check_actions,
_namespaced_rule_id,
_namespaced_rule_id_from_spec,
_priority_class_from_spec,
)
from synapse.storage import DataStore
from synapse.storage.background_updates import (
DEFAULT_BATCH_SIZE_CALLBACK,
Expand Down Expand Up @@ -192,6 +200,7 @@ def __init__(self, hs: "HomeServer", auth_handler: AuthHandler) -> None:
self._clock: Clock = hs.get_clock()
self._registration_handler = hs.get_registration_handler()
self._send_email_handler = hs.get_send_email_handler()
self._notifier = hs.get_notifier()
self.custom_template_dir = hs.config.server.custom_template_directory

try:
Expand Down Expand Up @@ -1340,6 +1349,91 @@ async def store_remote_3pid_association(
"""
await self._store.add_user_bound_threepid(user_id, medium, address, id_server)

async def add_push_rule_for_user(
babolivier marked this conversation as resolved.
Show resolved Hide resolved
self,
user_id: str,
scope: str,
kind: str,
rule_id: str,
conditions: List[Dict[str, Any]],
actions: List[Union[str, Dict[str, str]]],
before: Optional[str] = None,
after: Optional[str] = None,
) -> None:
"""Adds a new push rule for the given user.

See https://spec.matrix.org/latest/client-server-api/#push-rules for more
information about the push rules syntax.

This method can only be called on the main process.

Added in Synapse v1.58.0.

Args:
user_id: The ID of the user to add the push rule for.
scope: The push rule's scope. Currently, the only supported scope is "global".
kind: The rule's kind.
rule_id: The rule's identifier.
conditions: The conditions to match to trigger the actions.
actions: The actions to trigger if all conditions are met.
before: If set, the new rule will be the next most important rule relative to
the given rule. Must be a user-defined rule (as opposed to a server-defined
one).
after: If set, the new rule will be the next least important rule relative to
the given rule. Must be a user-defined rule (as opposed to a server-defined
one).

Raises:
synapse.module_api.errors.SynapseError if the module attempts to add a push
rule on a worker, or to add a push rule for a remote user.
synapse.module_api.errors.InvalidRuleException if the rule is not compliant
with the Matrix spec.
synapse.module_api.errors.RuleNotFoundException if the rule referred to by
before or after can't be found.
synapse.module_api.errors.InconsistentRuleException if the priority of the
rule's kind is not compatible with those of the rules referred to by
before or after.
"""
if not self.is_mine(user_id):
raise SynapseError(500, "Can only create push rules for local users")

if self.worker_app is not None:
raise SynapseError(500, "Can't create push rules on a worker")

# At this point we know we're on the main process so the store should not be a
# GenericWorkerSlavedStore.
assert isinstance(self._store, DataStore)

spec = RuleSpec(scope, kind, rule_id, None)
priority_class = _priority_class_from_spec(spec)

if spec.rule_id.startswith("."):
# Rule ids starting with '.' are reserved for server default rules.
raise InvalidRuleException(
400, "cannot add new rule_ids that start with '.'"
)

if before:
before = _namespaced_rule_id(spec, before)

if after:
after = _namespaced_rule_id(spec, after)

_check_actions(actions)

await self._store.add_push_rule(
user_id=user_id,
rule_id=_namespaced_rule_id_from_spec(spec),
priority_class=priority_class,
conditions=conditions,
actions=actions,
before=before,
after=after,
)

stream_id = self._store.get_max_push_rules_stream_id()
self._notifier.on_new_event("push_rules_key", stream_id, users=[user_id])


class PublicRoomListManager:
"""Contains methods for adding to, removing from and querying whether a room
Expand Down
5 changes: 5 additions & 0 deletions synapse/module_api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@
SynapseError,
)
from synapse.config._base import ConfigError
from synapse.rest.client.push_rule import InvalidRuleException
from synapse.storage.push_rule import InconsistentRuleException, RuleNotFoundException

__all__ = [
"InvalidClientCredentialsError",
"RedirectException",
"SynapseError",
"ConfigError",
"InvalidRuleException",
"RuleNotFoundException",
"InconsistentRuleException",
]
71 changes: 70 additions & 1 deletion tests/module_api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from synapse.federation.units import Transaction
from synapse.handlers.presence import UserPresenceState
from synapse.rest import admin
from synapse.rest.client import login, presence, profile, room
from synapse.rest.client import login, notifications, presence, profile, room
from synapse.types import create_requester

from tests.events.test_presence_router import send_presence_update, sync_presence
Expand All @@ -38,6 +38,7 @@ class ModuleApiTestCase(HomeserverTestCase):
room.register_servlets,
presence.register_servlets,
profile.register_servlets,
notifications.register_servlets,
]

def prepare(self, reactor, clock, homeserver):
Expand Down Expand Up @@ -553,6 +554,74 @@ def test_get_room_state(self):
self.assertEqual(state[("org.matrix.test", "")].state_key, "")
self.assertEqual(state[("org.matrix.test", "")].content, {})

def test_add_push_rule(self) -> None:
"""Test that a module can set a custom push rule for a user."""

# Create a room with 2 users in it. Push rules must not match if the user is the
# event's sender, so we need one user to send messages and one user to receive
# notifications.
user_id = self.register_user("user", "password")
tok = self.login("user", "password")

room_id = self.helper.create_room_as(user_id, is_public=True, tok=tok)

user_id2 = self.register_user("user2", "password")
tok2 = self.login("user2", "password")
self.helper.join(room_id, user_id2, tok=tok2)

# Set a push rule that doesn't notify for events with the word "testword" in it.
# This makes sense because we notify for every message by default, so to test
# that our change had any impact the easiest way is to not notify on something.
self.get_success(
defer.ensureDeferred(
self.module_api.add_push_rule_for_user(
user_id=user_id,
scope="global",
kind="content",
rule_id="test",
conditions=[
{
"kind": "event_match",
"key": "content.body",
"pattern": "testword",
}
],
actions=["dont_notify"],
)
)
)

# Send a message as the second user containing this word, and check that it
# didn't notify.
self.helper.send(room_id=room_id, body="here's a testword", tok=tok2)

channel = self.make_request(
"GET",
"/notifications",
access_token=tok,
)
self.assertEqual(channel.code, 200, channel.result)
self.assertEqual(len(channel.json_body["notifications"]), 0)

# Send a message as the second user not containing "testword" and check that it
# still notifies.
res = self.helper.send(room_id=room_id, body="here's a word", tok=tok2)
event_id = res["event_id"]

channel = self.make_request(
"GET",
"/notifications",
access_token=tok,
)
self.assertEqual(channel.code, 200, channel.result)

self.assertEqual(len(channel.json_body["notifications"]), 1, channel.json_body)
self.assertEqual(
channel.json_body["notifications"][0]["event"]["event_id"],
event_id,
channel.json_body,
)


class ModuleApiWorkerTestCase(BaseMultiWorkerStreamTestCase):
"""For testing ModuleApi functionality in a multi-worker setup"""
Expand Down