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

Commit 62c7b10

Browse files
committed
Allow modules to create and send events into rooms (#8479)
This PR allows Synapse modules making use of the `ModuleApi` to create and send non-membership events into a room. This can useful to have modules send messages, or change power levels in a room etc. Note that they must send event through a user that's already in the room. The non-membership event limitation is currently arbitrary, as it's another chunk of work and not necessary at the moment. This commit has been cherry-picked from mainline.
1 parent 11523b5 commit 62c7b10

File tree

7 files changed

+299
-89
lines changed

7 files changed

+299
-89
lines changed

changelog.d/8479.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add the ability to send non-membership events into a room via the `ModuleApi`.

synapse/events/third_party_rules.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ async def check_visibility_can_be_modified(
124124
if self.third_party_rules is None:
125125
return True
126126

127-
check_func = getattr(self.third_party_rules, "check_visibility_can_be_modified")
127+
check_func = getattr(
128+
self.third_party_rules, "check_visibility_can_be_modified", None
129+
)
128130
if not check_func or not isinstance(check_func, Callable):
129131
return True
130132

synapse/handlers/message.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
from ._base import BaseHandler
6464

6565
if TYPE_CHECKING:
66+
from synapse.events.third_party_rules import ThirdPartyEventRules
6667
from synapse.server import HomeServer
6768

6869
logger = logging.getLogger(__name__)
@@ -396,7 +397,9 @@ def __init__(self, hs: "HomeServer"):
396397
self.action_generator = hs.get_action_generator()
397398

398399
self.spam_checker = hs.get_spam_checker()
399-
self.third_party_event_rules = hs.get_third_party_event_rules()
400+
self.third_party_event_rules = (
401+
self.hs.get_third_party_event_rules()
402+
) # type: ThirdPartyEventRules
400403

401404
self._block_events_without_consent_error = (
402405
self.config.block_events_without_consent_error

synapse/module_api/__init__.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818

1919
from twisted.internet import defer
2020

21+
from synapse.events import EventBase
2122
from synapse.http.client import SimpleHttpClient
2223
from synapse.http.site import SynapseRequest
2324
from synapse.logging.context import make_deferred_yieldable, run_in_background
24-
from synapse.types import UserID
25+
from synapse.types import JsonDict, UserID, create_requester
2526

2627
if TYPE_CHECKING:
2728
from synapse.server import HomeServer
@@ -310,3 +311,30 @@ async def complete_sso_login_async(
310311
await self._auth_handler.complete_sso_login(
311312
registered_user_id, request, client_redirect_url,
312313
)
314+
315+
async def create_and_send_event_into_room(self, event_dict: JsonDict) -> EventBase:
316+
"""Create and send an event into a room. Membership events are currently not supported.
317+
318+
Args:
319+
event_dict: A dictionary representing the event to send.
320+
Required keys are `type`, `room_id`, `sender` and `content`.
321+
322+
Returns:
323+
The event that was sent. If state event deduplication happened, then
324+
the previous, duplicate event instead.
325+
326+
Raises:
327+
SynapseError if the event was not allowed.
328+
"""
329+
# Create a requester object
330+
requester = create_requester(event_dict["sender"])
331+
332+
# Create and send the event
333+
(
334+
event,
335+
_,
336+
) = await self._hs.get_event_creation_handler().create_and_send_nonmember_event(
337+
requester, event_dict, ratelimit=False
338+
)
339+
340+
return event

tests/module_api/test_api.py

+92
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@
1212
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
15+
from mock import Mock
16+
17+
from synapse.events import EventBase
1518
from synapse.module_api import ModuleApi
1619
from synapse.rest import admin
1720
from synapse.rest.client.v1 import login, room
21+
from synapse.types import create_requester
1822

1923
from tests.unittest import HomeserverTestCase
2024

@@ -29,6 +33,7 @@ class ModuleApiTestCase(HomeserverTestCase):
2933
def prepare(self, reactor, clock, homeserver):
3034
self.store = homeserver.get_datastore()
3135
self.module_api = ModuleApi(homeserver, homeserver.get_auth_handler())
36+
self.event_creation_handler = homeserver.get_event_creation_handler()
3237

3338
def test_can_register_user(self):
3439
"""Tests that an external module can register a user"""
@@ -60,6 +65,93 @@ def test_can_register_user(self):
6065
displayname = self.get_success(self.store.get_profile_displayname("bob"))
6166
self.assertEqual(displayname, "Bobberino")
6267

68+
def test_sending_events_into_room(self):
69+
"""Tests that a module can send events into a room"""
70+
# Mock out create_and_send_nonmember_event to check whether events are being sent
71+
self.event_creation_handler.create_and_send_nonmember_event = Mock(
72+
spec=[],
73+
side_effect=self.event_creation_handler.create_and_send_nonmember_event,
74+
)
75+
76+
# Create a user and room to play with
77+
user_id = self.register_user("summer", "monkey")
78+
tok = self.login("summer", "monkey")
79+
room_id = self.helper.create_room_as(user_id, tok=tok)
80+
81+
# Create and send a non-state event
82+
content = {"body": "I am a puppet", "msgtype": "m.text"}
83+
event_dict = {
84+
"room_id": room_id,
85+
"type": "m.room.message",
86+
"content": content,
87+
"sender": user_id,
88+
}
89+
event = self.get_success(
90+
self.module_api.create_and_send_event_into_room(event_dict)
91+
) # type: EventBase
92+
self.assertEqual(event.sender, user_id)
93+
self.assertEqual(event.type, "m.room.message")
94+
self.assertEqual(event.room_id, room_id)
95+
self.assertFalse(hasattr(event, "state_key"))
96+
self.assertDictEqual(event.content, content)
97+
98+
# Check that the event was sent
99+
self.event_creation_handler.create_and_send_nonmember_event.assert_called_with(
100+
create_requester(user_id), event_dict, ratelimit=False,
101+
)
102+
103+
# Create and send a state event
104+
content = {
105+
"events_default": 0,
106+
"users": {user_id: 100},
107+
"state_default": 50,
108+
"users_default": 0,
109+
"events": {"test.event.type": 25},
110+
}
111+
event_dict = {
112+
"room_id": room_id,
113+
"type": "m.room.power_levels",
114+
"content": content,
115+
"sender": user_id,
116+
"state_key": "",
117+
}
118+
event = self.get_success(
119+
self.module_api.create_and_send_event_into_room(event_dict)
120+
) # type: EventBase
121+
self.assertEqual(event.sender, user_id)
122+
self.assertEqual(event.type, "m.room.power_levels")
123+
self.assertEqual(event.room_id, room_id)
124+
self.assertEqual(event.state_key, "")
125+
self.assertDictEqual(event.content, content)
126+
127+
# Check that the event was sent
128+
self.event_creation_handler.create_and_send_nonmember_event.assert_called_with(
129+
create_requester(user_id),
130+
{
131+
"type": "m.room.power_levels",
132+
"content": content,
133+
"room_id": room_id,
134+
"sender": user_id,
135+
"state_key": "",
136+
},
137+
ratelimit=False,
138+
)
139+
140+
# Check that we can't send membership events
141+
content = {
142+
"membership": "leave",
143+
}
144+
event_dict = {
145+
"room_id": room_id,
146+
"type": "m.room.member",
147+
"content": content,
148+
"sender": user_id,
149+
"state_key": user_id,
150+
}
151+
self.get_failure(
152+
self.module_api.create_and_send_event_into_room(event_dict), Exception
153+
)
154+
63155
def test_public_rooms(self):
64156
"""Tests that a room can be added and removed from the public rooms list,
65157
as well as have its public rooms directory state queried.
+170
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2019 The Matrix.org Foundation C.I.C.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the 'License');
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an 'AS IS' BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
import threading
16+
from typing import Dict
17+
18+
from mock import Mock
19+
20+
from synapse.events import EventBase
21+
from synapse.module_api import ModuleApi
22+
from synapse.rest import admin
23+
from synapse.rest.client.v1 import login, room
24+
from synapse.types import Requester, StateMap
25+
26+
from tests import unittest
27+
28+
thread_local = threading.local()
29+
30+
31+
class ThirdPartyRulesTestModule:
32+
def __init__(self, config: Dict, module_api: ModuleApi):
33+
# keep a record of the "current" rules module, so that the test can patch
34+
# it if desired.
35+
thread_local.rules_module = self
36+
self.module_api = module_api
37+
38+
async def on_create_room(
39+
self, requester: Requester, config: dict, is_requester_admin: bool
40+
):
41+
return True
42+
43+
async def check_event_allowed(self, event: EventBase, state: StateMap[EventBase]):
44+
return True
45+
46+
@staticmethod
47+
def parse_config(config):
48+
return config
49+
50+
51+
def current_rules_module() -> ThirdPartyRulesTestModule:
52+
return thread_local.rules_module
53+
54+
55+
class ThirdPartyRulesTestCase(unittest.HomeserverTestCase):
56+
servlets = [
57+
admin.register_servlets,
58+
login.register_servlets,
59+
room.register_servlets,
60+
]
61+
62+
def default_config(self):
63+
config = super().default_config()
64+
config["third_party_event_rules"] = {
65+
"module": __name__ + ".ThirdPartyRulesTestModule",
66+
"config": {},
67+
}
68+
return config
69+
70+
def prepare(self, reactor, clock, homeserver):
71+
# Create a user and room to play with during the tests
72+
self.user_id = self.register_user("kermit", "monkey")
73+
self.tok = self.login("kermit", "monkey")
74+
75+
self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok)
76+
77+
def test_third_party_rules(self):
78+
"""Tests that a forbidden event is forbidden from being sent, but an allowed one
79+
can be sent.
80+
"""
81+
# patch the rules module with a Mock which will return False for some event
82+
# types
83+
async def check(ev, state):
84+
return ev.type != "foo.bar.forbidden"
85+
86+
callback = Mock(spec=[], side_effect=check)
87+
current_rules_module().check_event_allowed = callback
88+
89+
request, channel = self.make_request(
90+
"PUT",
91+
"/_matrix/client/r0/rooms/%s/send/foo.bar.allowed/1" % self.room_id,
92+
{},
93+
access_token=self.tok,
94+
)
95+
self.render(request)
96+
self.assertEquals(channel.result["code"], b"200", channel.result)
97+
98+
callback.assert_called_once()
99+
100+
# there should be various state events in the state arg: do some basic checks
101+
state_arg = callback.call_args[0][1]
102+
for k in (("m.room.create", ""), ("m.room.member", self.user_id)):
103+
self.assertIn(k, state_arg)
104+
ev = state_arg[k]
105+
self.assertEqual(ev.type, k[0])
106+
self.assertEqual(ev.state_key, k[1])
107+
108+
request, channel = self.make_request(
109+
"PUT",
110+
"/_matrix/client/r0/rooms/%s/send/foo.bar.forbidden/1" % self.room_id,
111+
{},
112+
access_token=self.tok,
113+
)
114+
self.render(request)
115+
self.assertEquals(channel.result["code"], b"403", channel.result)
116+
117+
def test_modify_event(self):
118+
"""Tests that the module can successfully tweak an event before it is persisted.
119+
"""
120+
# first patch the event checker so that it will modify the event
121+
async def check(ev: EventBase, state):
122+
ev.content = {"x": "y"}
123+
return True
124+
125+
current_rules_module().check_event_allowed = check
126+
127+
# now send the event
128+
request, channel = self.make_request(
129+
"PUT",
130+
"/_matrix/client/r0/rooms/%s/send/modifyme/1" % self.room_id,
131+
{"x": "x"},
132+
access_token=self.tok,
133+
)
134+
self.render(request)
135+
self.assertEqual(channel.result["code"], b"200", channel.result)
136+
event_id = channel.json_body["event_id"]
137+
138+
# ... and check that it got modified
139+
request, channel = self.make_request(
140+
"GET",
141+
"/_matrix/client/r0/rooms/%s/event/%s" % (self.room_id, event_id),
142+
access_token=self.tok,
143+
)
144+
self.render(request)
145+
self.assertEqual(channel.result["code"], b"200", channel.result)
146+
ev = channel.json_body
147+
self.assertEqual(ev["content"]["x"], "y")
148+
149+
def test_send_event(self):
150+
"""Tests that the module can send an event into a room via the module api"""
151+
content = {
152+
"msgtype": "m.text",
153+
"body": "Hello!",
154+
}
155+
event_dict = {
156+
"room_id": self.room_id,
157+
"type": "m.room.message",
158+
"content": content,
159+
"sender": self.user_id,
160+
}
161+
event = self.get_success(
162+
current_rules_module().module_api.create_and_send_event_into_room(
163+
event_dict
164+
)
165+
) # type: EventBase
166+
167+
self.assertEquals(event.sender, self.user_id)
168+
self.assertEquals(event.room_id, self.room_id)
169+
self.assertEquals(event.type, "m.room.message")
170+
self.assertEquals(event.content, content)

0 commit comments

Comments
 (0)