From c44384b4f9f54aed84d3c0a657ff03e2caec2e99 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 19 Oct 2021 18:13:49 +0100 Subject: [PATCH 01/11] Implement an on_new_event callback --- docs/modules/third_party_rules_callbacks.md | 19 ++++++++++++ synapse/api/errors.py | 7 +++++ synapse/events/third_party_rules.py | 31 +++++++++++++++++++- synapse/handlers/federation_event.py | 2 +- synapse/handlers/message.py | 4 +-- synapse/notifier.py | 10 +++++-- tests/rest/client/test_third_party_rules.py | 32 +++++++++++++++++++-- 7 files changed, 97 insertions(+), 8 deletions(-) diff --git a/docs/modules/third_party_rules_callbacks.md b/docs/modules/third_party_rules_callbacks.md index 5371e7f80707..c822a01ccb64 100644 --- a/docs/modules/third_party_rules_callbacks.md +++ b/docs/modules/third_party_rules_callbacks.md @@ -111,6 +111,25 @@ callback returns `True`, Synapse falls through to the next one. The value of the callback that does not return `True` will be used. If this happens, Synapse will not call any of the subsequent implementations of this callback. +### `on_new_event` + +```python +async def on_new_event( + event: "synapse.events.EventBase", + state_events: "synapse.types.StateMap", +) -> None: +``` + +Called after sending an event into a room. The module is passed the invite event, as well +as the state of the room _after_ the event. This means that if the event is a state event, +it will be included in this state. + +Note that this callback is called when the event has already been processed and stored +into the room, which means this callback cannot be used to deny it. To deny an incoming +event, see [`check_event_for_spam`](http://localhost:3000/modules/spam_checker_callbacks.html#check_event_for_spam). + +If multiple modules implement this callback, Synapse runs them all in order. + ## Example The example below is a module that implements the third-party rules callback diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 685d1c25cf9f..7b6a73af751d 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -596,3 +596,10 @@ class ShadowBanError(Exception): This should be caught and a proper "fake" success response sent to the user. """ + + +class ModuleFailureError(Exception): + """ + Raised when a module raises an exception. If this is raised in the context of an + HTTP(S) request, it will translate into a 500 response with a generic Matrix error. + """ diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index 2a6dabdab654..e97029ab558c 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -14,7 +14,7 @@ import logging from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tuple -from synapse.api.errors import SynapseError +from synapse.api.errors import ModuleFailureError, SynapseError from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.types import Requester, StateMap @@ -36,6 +36,7 @@ CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK = Callable[ [str, StateMap[EventBase], str], Awaitable[bool] ] +ON_NEW_EVENT_CALLBACK = Callable[[EventBase, StateMap[EventBase]], Awaitable] def load_legacy_third_party_event_rules(hs: "HomeServer") -> None: @@ -152,6 +153,7 @@ def __init__(self, hs: "HomeServer"): self._check_visibility_can_be_modified_callbacks: List[ CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK ] = [] + self._on_new_event_callbacks: List[ON_NEW_EVENT_CALLBACK] = [] def register_third_party_rules_callbacks( self, @@ -163,6 +165,7 @@ def register_third_party_rules_callbacks( check_visibility_can_be_modified: Optional[ CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK ] = None, + on_new_event: Optional[ON_NEW_EVENT_CALLBACK] = None, ) -> None: """Register callbacks from modules for each hook.""" if check_event_allowed is not None: @@ -181,6 +184,9 @@ def register_third_party_rules_callbacks( check_visibility_can_be_modified, ) + if on_new_event is not None: + self._on_new_event_callbacks.append(on_new_event) + async def check_event_allowed( self, event: EventBase, context: EventContext ) -> Tuple[bool, Optional[dict]]: @@ -321,6 +327,29 @@ async def check_visibility_can_be_modified( return True + async def on_new_event(self, event: EventBase): + """Let modules act on events after they've been sent (e.g. auto-accepting + invites, etc.) + + Args: + event: The invite event. + + Raises: + ModuleFailureError if a callback raised any exception. + """ + # Bail out early without hitting the store if we don't have any callback + if len(self._on_new_event_callbacks) == 0: + return True + + state_events = self._get_state_map_for_room(event.room_id) + + for callback in self._on_new_event_callbacks: + try: + await callback(event, state_events) + except Exception as e: + logger.error("Failed to run module API callback %s: %s", callback, e) + raise ModuleFailureError(e) + async def _get_state_map_for_room(self, room_id: str) -> StateMap[EventBase]: """Given a room ID, return the state events of that room. diff --git a/synapse/handlers/federation_event.py b/synapse/handlers/federation_event.py index 5a2f2e5ebb77..f593d4e0e540 100644 --- a/synapse/handlers/federation_event.py +++ b/synapse/handlers/federation_event.py @@ -2019,7 +2019,7 @@ async def _notify_persisted_event( event_pos = PersistedEventPosition( self._instance_name, event.internal_metadata.stream_ordering ) - self._notifier.on_new_room_event( + await self._notifier.on_new_room_event( event, event_pos, max_stream_token, extra_users=extra_users ) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 2e024b551f99..0a9d5f048632 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1537,9 +1537,9 @@ async def persist_and_notify_client_event( # If there's an expiry timestamp on the event, schedule its expiry. self._message_handler.maybe_schedule_expiry(event) - def _notify() -> None: + async def _notify() -> None: try: - self.notifier.on_new_room_event( + await self.notifier.on_new_room_event( event, event_pos, max_stream_token, extra_users=extra_users ) except Exception: diff --git a/synapse/notifier.py b/synapse/notifier.py index 1a9f84ba4533..e125670c4560 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -220,6 +220,8 @@ def __init__(self, hs: "synapse.server.HomeServer"): # down. self.remote_server_up_callbacks: List[Callable[[str], None]] = [] + self._third_party_rules = hs.get_third_party_event_rules() + self.clock = hs.get_clock() self.appservice_handler = hs.get_application_service_handler() self._pusher_pool = hs.get_pusherpool() @@ -267,14 +269,16 @@ def add_replication_callback(self, cb: Callable[[], None]): """ self.replication_callbacks.append(cb) - def on_new_room_event( + async def on_new_room_event( self, event: EventBase, event_pos: PersistedEventPosition, max_room_stream_token: RoomStreamToken, extra_users: Optional[Collection[UserID]] = None, ): - """Unwraps event and calls `on_new_room_event_args`.""" + """Unwraps event and calls `on_new_room_event_args`. + Also notifies modules listening on new events via the `on_new_event` callback. + """ self.on_new_room_event_args( event_pos=event_pos, room_id=event.room_id, @@ -285,6 +289,8 @@ def on_new_room_event( extra_users=extra_users or [], ) + await self._third_party_rules.on_new_event(event) + def on_new_room_event_args( self, room_id: str, diff --git a/tests/rest/client/test_third_party_rules.py b/tests/rest/client/test_third_party_rules.py index 531f09c48b87..b8d0e9abbb98 100644 --- a/tests/rest/client/test_third_party_rules.py +++ b/tests/rest/client/test_third_party_rules.py @@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Dict, Optional, Tuple from unittest.mock import Mock -from synapse.api.constants import EventTypes +from synapse.api.constants import EventTypes, Membership from synapse.api.errors import SynapseError from synapse.events import EventBase from synapse.events.third_party_rules import load_legacy_third_party_event_rules @@ -25,6 +25,7 @@ from synapse.util.frozenutils import unfreeze from tests import unittest +from tests.test_utils import make_awaitable if TYPE_CHECKING: from synapse.module_api import ModuleApi @@ -89,8 +90,9 @@ def make_homeserver(self, reactor, clock): return hs def prepare(self, reactor, clock, homeserver): - # Create a user and room to play with during the tests + # Create some users and a room to play with during the tests self.user_id = self.register_user("kermit", "monkey") + self.invitee = self.register_user("invitee", "hackme") self.tok = self.login("kermit", "monkey") # Some tests might prevent room creation on purpose. @@ -424,6 +426,32 @@ async def test_fn(event: EventBase, state_events): self.assertEqual(channel.code, 200) self.assertEqual(channel.json_body["i"], i) + def test_on_new_event(self): + """Test that the on_new_event callback is called on new events""" + on_new_event = Mock(make_awaitable(None)) + self.hs.get_third_party_event_rules()._on_new_event_callbacks.append( + on_new_event + ) + + # Send a message event to the room and check that the callback is called. + self.helper.send(room_id=self.room_id, tok=self.tok) + self.assertEqual(on_new_event.call_count, 1) + + # Check that the callback is also called on membership updates. + self.helper.invite( + room=self.room_id, + src=self.user_id, + targ=self.invitee, + tok=self.tok, + ) + + self.assertEqual(on_new_event.call_count, 2) + + args, _ = on_new_event.call_args + + self.assertEqual(args[0].membership, Membership.INVITE) + self.assertEqual(args[0].state_key, self.invitee) + def _update_power_levels(self, event_default: int = 0): """Updates the room's power levels. From a4c588964571fb26c0befb06477ab8e0b0f6da89 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 19 Oct 2021 18:15:17 +0100 Subject: [PATCH 02/11] Changelog --- changelog.d/11125.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/11125.feature diff --git a/changelog.d/11125.feature b/changelog.d/11125.feature new file mode 100644 index 000000000000..c42422839000 --- /dev/null +++ b/changelog.d/11125.feature @@ -0,0 +1 @@ +Add an `on_new_event` third-party rules callback to allow modules to listen on new events. From 2efd5fe5fd8af34875ce5da90a1ac1762a865be4 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 19 Oct 2021 18:16:28 +0100 Subject: [PATCH 03/11] Fix changelog number --- changelog.d/{11125.feature => 11126.feature} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{11125.feature => 11126.feature} (100%) diff --git a/changelog.d/11125.feature b/changelog.d/11126.feature similarity index 100% rename from changelog.d/11125.feature rename to changelog.d/11126.feature From d0107abb4c070d4e5865f465cac71736e00af012 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 19 Oct 2021 18:24:31 +0100 Subject: [PATCH 04/11] mypy --- synapse/events/third_party_rules.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index e97029ab558c..1b3f95785b51 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -327,7 +327,7 @@ async def check_visibility_can_be_modified( return True - async def on_new_event(self, event: EventBase): + async def on_new_event(self, event: EventBase) -> None: """Let modules act on events after they've been sent (e.g. auto-accepting invites, etc.) @@ -339,9 +339,9 @@ async def on_new_event(self, event: EventBase): """ # Bail out early without hitting the store if we don't have any callback if len(self._on_new_event_callbacks) == 0: - return True + return - state_events = self._get_state_map_for_room(event.room_id) + state_events = await self._get_state_map_for_room(event.room_id) for callback in self._on_new_event_callbacks: try: From 0fe5b512fe1820037b5b065948237d2e0001d9c0 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Oct 2021 15:49:28 +0200 Subject: [PATCH 05/11] Apply suggestions from code review Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- changelog.d/11126.feature | 2 +- docs/modules/third_party_rules_callbacks.md | 4 ++-- synapse/events/third_party_rules.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/changelog.d/11126.feature b/changelog.d/11126.feature index c42422839000..7290941665ed 100644 --- a/changelog.d/11126.feature +++ b/changelog.d/11126.feature @@ -1 +1 @@ -Add an `on_new_event` third-party rules callback to allow modules to listen on new events. +Add an `on_new_event` third-party rules callback to allow modules to act after an event has been sent into a room. diff --git a/docs/modules/third_party_rules_callbacks.md b/docs/modules/third_party_rules_callbacks.md index c822a01ccb64..53a3496508fa 100644 --- a/docs/modules/third_party_rules_callbacks.md +++ b/docs/modules/third_party_rules_callbacks.md @@ -120,12 +120,12 @@ async def on_new_event( ) -> None: ``` -Called after sending an event into a room. The module is passed the invite event, as well +Called after sending an event into a room. The module is passed the event, as well as the state of the room _after_ the event. This means that if the event is a state event, it will be included in this state. Note that this callback is called when the event has already been processed and stored -into the room, which means this callback cannot be used to deny it. To deny an incoming +into the room, which means this callback cannot be used to deny persisting the event. To deny an incoming event, see [`check_event_for_spam`](http://localhost:3000/modules/spam_checker_callbacks.html#check_event_for_spam). If multiple modules implement this callback, Synapse runs them all in order. diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index 1b3f95785b51..cecb35abb91e 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -332,12 +332,12 @@ async def on_new_event(self, event: EventBase) -> None: invites, etc.) Args: - event: The invite event. + event: The event. Raises: ModuleFailureError if a callback raised any exception. """ - # Bail out early without hitting the store if we don't have any callback + # Bail out early without hitting the store if we don't have any callbacks if len(self._on_new_event_callbacks) == 0: return From e08dbe6d6b53cd7005c8f38e9058b1f92fd82b2d Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Oct 2021 14:58:13 +0100 Subject: [PATCH 06/11] Fix link in doc --- docs/modules/third_party_rules_callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/third_party_rules_callbacks.md b/docs/modules/third_party_rules_callbacks.md index 53a3496508fa..5d28f7f6713c 100644 --- a/docs/modules/third_party_rules_callbacks.md +++ b/docs/modules/third_party_rules_callbacks.md @@ -125,8 +125,8 @@ as the state of the room _after_ the event. This means that if the event is a st it will be included in this state. Note that this callback is called when the event has already been processed and stored -into the room, which means this callback cannot be used to deny persisting the event. To deny an incoming -event, see [`check_event_for_spam`](http://localhost:3000/modules/spam_checker_callbacks.html#check_event_for_spam). +into the room, which means this callback cannot be used to deny persisting the event. To +deny an incoming event, see [`check_event_for_spam`](spam_checker_callbacks.md#check_event_for_spam) instead. If multiple modules implement this callback, Synapse runs them all in order. From b9421ecb2298941a901ef5bfc598c330a03bc8d9 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Oct 2021 16:48:09 +0100 Subject: [PATCH 07/11] Incorporate review --- synapse/api/errors.py | 7 --- synapse/events/third_party_rules.py | 5 +- synapse/handlers/message.py | 5 +- tests/rest/client/test_third_party_rules.py | 66 ++++++++++++++++++++- 4 files changed, 70 insertions(+), 13 deletions(-) diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 7b6a73af751d..685d1c25cf9f 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -596,10 +596,3 @@ class ShadowBanError(Exception): This should be caught and a proper "fake" success response sent to the user. """ - - -class ModuleFailureError(Exception): - """ - Raised when a module raises an exception. If this is raised in the context of an - HTTP(S) request, it will translate into a 500 response with a generic Matrix error. - """ diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index cecb35abb91e..378bfdc279f9 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -14,7 +14,7 @@ import logging from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tuple -from synapse.api.errors import ModuleFailureError, SynapseError +from synapse.api.errors import SynapseError from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.types import Requester, StateMap @@ -347,8 +347,7 @@ async def on_new_event(self, event: EventBase) -> None: try: await callback(event, state_events) except Exception as e: - logger.error("Failed to run module API callback %s: %s", callback, e) - raise ModuleFailureError(e) + logger.exception("Failed to run module API callback %s: %s", callback, e) async def _get_state_map_for_room(self, room_id: str) -> StateMap[EventBase]: """Given a room ID, return the state events of that room. diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 0a9d5f048632..4a0fccfcc6ac 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -1543,7 +1543,10 @@ async def _notify() -> None: event, event_pos, max_stream_token, extra_users=extra_users ) except Exception: - logger.exception("Error notifying about new room event") + logger.exception( + "Error notifying about new room event %s", + event.event_id, + ) run_in_background(_notify) diff --git a/tests/rest/client/test_third_party_rules.py b/tests/rest/client/test_third_party_rules.py index b8d0e9abbb98..0d161d618ff6 100644 --- a/tests/rest/client/test_third_party_rules.py +++ b/tests/rest/client/test_third_party_rules.py @@ -17,7 +17,8 @@ from synapse.api.constants import EventTypes, Membership from synapse.api.errors import SynapseError -from synapse.events import EventBase +from synapse.api.room_versions import RoomVersions +from synapse.events import EventBase, FrozenEventV3 from synapse.events.third_party_rules import load_legacy_third_party_event_rules from synapse.rest import admin from synapse.rest.client import login, room @@ -75,7 +76,7 @@ async def check_event_allowed(self, event: EventBase, state: StateMap[EventBase] return d -class ThirdPartyRulesTestCase(unittest.HomeserverTestCase): +class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): servlets = [ admin.register_servlets, login.register_servlets, @@ -87,6 +88,25 @@ def make_homeserver(self, reactor, clock): load_legacy_third_party_event_rules(hs) + # We're not going to be properly signing events as our remote homeserver is fake, + # therefore disable event signature checks. + # Note that these checks are not relevant to this test case. + + # Have this homeserver auto-approve all event signature checking. + async def approve_all_signature_checking(_, pdu): + return pdu + + hs.get_federation_server()._check_sigs_and_hash = ( + approve_all_signature_checking + ) + + # Have this homeserver skip event auth checks. This is necessary due to + # event auth checks ensuring that events were signed by the sender's homeserver. + async def _check_event_auth(origin, event, context, *args, **kwargs): + return context + + hs.get_federation_event_handler()._check_event_auth = _check_event_auth + return hs def prepare(self, reactor, clock, homeserver): @@ -452,6 +472,48 @@ def test_on_new_event(self): self.assertEqual(args[0].membership, Membership.INVITE) self.assertEqual(args[0].state_key, self.invitee) + # Check that the invitee's membership is correct in the state that's passed down + # to the callback. + self.assertEqual( + args[1][(EventTypes.Member, self.invitee)].membership, + Membership.INVITE, + ) + + # Send an event over federation and check that the callback is also called. + self._send_event_over_federation() + self.assertEqual(on_new_event.call_count, 3) + + def _send_event_over_federation(self) -> None: + """Send a dummy event over federation and check that the request succeeds.""" + body = { + "origin": self.hs.config.server.server_name, + "origin_server_ts": self.clock.time_msec(), + "pdus": [ + { + "sender": self.user_id, + "type": EventTypes.Message, + "state_key": "", + "content": {"body": "hello world", "msgtype": "m.text"}, + "room_id": self.room_id, + "depth": 0, + "origin_server_ts": self.clock.time_msec(), + "prev_events": [], + "auth_events": [], + "signatures": {}, + "unsigned": {}, + } + ], + } + + channel = self.make_request( + method="PUT", + path="/_matrix/federation/v1/send/1", + content=body, + federation_auth_origin=self.hs.config.server.server_name.encode("utf8"), + ) + + self.assertEqual(channel.code, 200, channel.result) + def _update_power_levels(self, event_default: int = 0): """Updates the room's power levels. From 792125599d0c19848373433d3c5e96c134d4cf7f Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Wed, 20 Oct 2021 16:51:32 +0100 Subject: [PATCH 08/11] Lint --- synapse/events/third_party_rules.py | 4 +++- tests/rest/client/test_third_party_rules.py | 7 ++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index 378bfdc279f9..3ee0ba9737ff 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -347,7 +347,9 @@ async def on_new_event(self, event: EventBase) -> None: try: await callback(event, state_events) except Exception as e: - logger.exception("Failed to run module API callback %s: %s", callback, e) + logger.exception( + "Failed to run module API callback %s: %s", callback, e + ) async def _get_state_map_for_room(self, room_id: str) -> StateMap[EventBase]: """Given a room ID, return the state events of that room. diff --git a/tests/rest/client/test_third_party_rules.py b/tests/rest/client/test_third_party_rules.py index 0d161d618ff6..1c42c4663005 100644 --- a/tests/rest/client/test_third_party_rules.py +++ b/tests/rest/client/test_third_party_rules.py @@ -17,8 +17,7 @@ from synapse.api.constants import EventTypes, Membership from synapse.api.errors import SynapseError -from synapse.api.room_versions import RoomVersions -from synapse.events import EventBase, FrozenEventV3 +from synapse.events import EventBase from synapse.events.third_party_rules import load_legacy_third_party_event_rules from synapse.rest import admin from synapse.rest.client import login, room @@ -96,9 +95,7 @@ def make_homeserver(self, reactor, clock): async def approve_all_signature_checking(_, pdu): return pdu - hs.get_federation_server()._check_sigs_and_hash = ( - approve_all_signature_checking - ) + hs.get_federation_server()._check_sigs_and_hash = approve_all_signature_checking # Have this homeserver skip event auth checks. This is necessary due to # event auth checks ensuring that events were signed by the sender's homeserver. From acd5ea83bb793fbe4fd13d3085bb50554150a001 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Fri, 22 Oct 2021 15:55:25 +0100 Subject: [PATCH 09/11] Call callback from on_new_room_event_args --- synapse/events/third_party_rules.py | 5 +++-- synapse/notifier.py | 19 +++++++++++-------- synapse/replication/tcp/client.py | 3 ++- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index 3ee0ba9737ff..8816ef4b7643 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -327,12 +327,12 @@ async def check_visibility_can_be_modified( return True - async def on_new_event(self, event: EventBase) -> None: + async def on_new_event(self, event_id: str) -> None: """Let modules act on events after they've been sent (e.g. auto-accepting invites, etc.) Args: - event: The event. + event_id: The ID of the event. Raises: ModuleFailureError if a callback raised any exception. @@ -341,6 +341,7 @@ async def on_new_event(self, event: EventBase) -> None: if len(self._on_new_event_callbacks) == 0: return + event = await self.store.get_event(event_id) state_events = await self._get_state_map_for_room(event.room_id) for callback in self._on_new_event_callbacks: diff --git a/synapse/notifier.py b/synapse/notifier.py index e125670c4560..f5618bcb23c4 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -276,12 +276,11 @@ async def on_new_room_event( max_room_stream_token: RoomStreamToken, extra_users: Optional[Collection[UserID]] = None, ): - """Unwraps event and calls `on_new_room_event_args`. - Also notifies modules listening on new events via the `on_new_event` callback. - """ - self.on_new_room_event_args( + """Unwraps event and calls `on_new_room_event_args`.""" + await self.on_new_room_event_args( event_pos=event_pos, room_id=event.room_id, + event_id=event.event_id, event_type=event.type, state_key=event.get("state_key"), membership=event.content.get("membership"), @@ -289,11 +288,10 @@ async def on_new_room_event( extra_users=extra_users or [], ) - await self._third_party_rules.on_new_event(event) - - def on_new_room_event_args( + async def on_new_room_event_args( self, room_id: str, + event_id: str, event_type: str, state_key: Optional[str], membership: Optional[str], @@ -308,7 +306,10 @@ def on_new_room_event_args( listening to the room, and any listeners for the users in the `extra_users` param. - The events can be peristed out of order. The notifier will wait + This also notifies modules listening on new events via the + `on_new_event` callback. + + The events can be persisted out of order. The notifier will wait until all previous events have been persisted before notifying the client streams. """ @@ -324,6 +325,8 @@ def on_new_room_event_args( ) self._notify_pending_new_room_events(max_room_stream_token) + await self._third_party_rules.on_new_event(event_id) + self.notify_replication() def _notify_pending_new_room_events(self, max_room_stream_token: RoomStreamToken): diff --git a/synapse/replication/tcp/client.py b/synapse/replication/tcp/client.py index 961c17762ede..e29ae1e375af 100644 --- a/synapse/replication/tcp/client.py +++ b/synapse/replication/tcp/client.py @@ -207,11 +207,12 @@ async def on_rdata( max_token = self.store.get_room_max_token() event_pos = PersistedEventPosition(instance_name, token) - self.notifier.on_new_room_event_args( + await self.notifier.on_new_room_event_args( event_pos=event_pos, max_room_stream_token=max_token, extra_users=extra_users, room_id=row.data.room_id, + event_id=row.data.event_id, event_type=row.data.type, state_key=row.data.state_key, membership=row.data.membership, From 05509f2cff05ea3cc045b5481be7241afd3d86e6 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Mon, 25 Oct 2021 15:11:01 +0100 Subject: [PATCH 10/11] Document when the feature was introduced --- docs/modules/third_party_rules_callbacks.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/modules/third_party_rules_callbacks.md b/docs/modules/third_party_rules_callbacks.md index 992af12c7d53..a16e272f794b 100644 --- a/docs/modules/third_party_rules_callbacks.md +++ b/docs/modules/third_party_rules_callbacks.md @@ -121,6 +121,8 @@ any of the subsequent implementations of this callback. ### `on_new_event` +_First introduced in Synapse v1.47.0_ + ```python async def on_new_event( event: "synapse.events.EventBase", From 4edd65f1a3f1c6bce2369b103e3320c85c605c06 Mon Sep 17 00:00:00 2001 From: Brendan Abolivier Date: Tue, 26 Oct 2021 14:44:12 +0200 Subject: [PATCH 11/11] Update changelog.d/11126.feature Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com> --- changelog.d/11126.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/11126.feature b/changelog.d/11126.feature index 7290941665ed..c6078fe081e8 100644 --- a/changelog.d/11126.feature +++ b/changelog.d/11126.feature @@ -1 +1 @@ -Add an `on_new_event` third-party rules callback to allow modules to act after an event has been sent into a room. +Add an `on_new_event` third-party rules callback to allow Synapse modules to act after an event has been sent into a room.