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

Commit 66ac4b1

Browse files
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.
1 parent 5009ffc commit 66ac4b1

File tree

5 files changed

+157
-8
lines changed

5 files changed

+157
-8
lines changed

changelog.d/8479.feature

Lines changed: 1 addition & 0 deletions
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/handlers/message.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from ._base import BaseHandler
6060

6161
if TYPE_CHECKING:
62+
from synapse.events.third_party_rules import ThirdPartyEventRules
6263
from synapse.server import HomeServer
6364

6465
logger = logging.getLogger(__name__)
@@ -393,7 +394,9 @@ def __init__(self, hs: "HomeServer"):
393394
self.action_generator = hs.get_action_generator()
394395

395396
self.spam_checker = hs.get_spam_checker()
396-
self.third_party_event_rules = hs.get_third_party_event_rules()
397+
self.third_party_event_rules = (
398+
self.hs.get_third_party_event_rules()
399+
) # type: ThirdPartyEventRules
397400

398401
self._block_events_without_consent_error = (
399402
self.config.block_events_without_consent_error
@@ -1229,11 +1232,7 @@ async def _send_dummy_event_for_room(self, room_id: str) -> bool:
12291232
# Since this is a dummy-event it is OK if it is sent by a
12301233
# shadow-banned user.
12311234
await self.handle_new_client_event(
1232-
requester=requester,
1233-
event=event,
1234-
context=context,
1235-
ratelimit=False,
1236-
ignore_shadow_ban=True,
1235+
requester, event, context, ratelimit=False, ignore_shadow_ban=True,
12371236
)
12381237
return True
12391238
except ConsentNotGivenError:

synapse/module_api/__init__.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
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
2425
from synapse.storage.state import StateFilter
25-
from synapse.types import UserID
26+
from synapse.types import JsonDict, UserID, create_requester
2627

2728
if TYPE_CHECKING:
2829
from synapse.server import HomeServer
@@ -320,6 +321,33 @@ def get_state_events_in_room(
320321
state = yield defer.ensureDeferred(self._store.get_events(state_ids.values()))
321322
return state.values()
322323

324+
async def create_and_send_event_into_room(self, event_dict: JsonDict) -> EventBase:
325+
"""Create and send an event into a room. Membership events are currently not supported.
326+
327+
Args:
328+
event_dict: A dictionary representing the event to send.
329+
Required keys are `type`, `room_id`, `sender` and `content`.
330+
331+
Returns:
332+
The event that was sent. If state event deduplication happened, then
333+
the previous, duplicate event instead.
334+
335+
Raises:
336+
SynapseError if the event was not allowed.
337+
"""
338+
# Create a requester object
339+
requester = create_requester(event_dict["sender"])
340+
341+
# Create and send the event
342+
(
343+
event,
344+
_,
345+
) = await self._hs.get_event_creation_handler().create_and_send_nonmember_event(
346+
requester, event_dict, ratelimit=False, ignore_shadow_ban=True,
347+
)
348+
349+
return event
350+
323351

324352
class PublicRoomListManager:
325353
"""Contains methods for adding to, removing from and querying whether a room

tests/module_api/test_api.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@
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
1516

17+
from synapse.events import EventBase
1618
from synapse.rest import admin
1719
from synapse.rest.client.v1 import login, room
20+
from synapse.types import create_requester
1821

1922
from tests.unittest import HomeserverTestCase
2023

@@ -29,6 +32,7 @@ class ModuleApiTestCase(HomeserverTestCase):
2932
def prepare(self, reactor, clock, homeserver):
3033
self.store = homeserver.get_datastore()
3134
self.module_api = homeserver.get_module_api()
35+
self.event_creation_handler = homeserver.get_event_creation_handler()
3236

3337
def test_can_register_user(self):
3438
"""Tests that an external module can register a user"""
@@ -60,6 +64,97 @@ def test_can_register_user(self):
6064
displayname = self.get_success(self.store.get_profile_displayname("bob"))
6165
self.assertEqual(displayname, "Bobberino")
6266

67+
def test_sending_events_into_room(self):
68+
"""Tests that a module can send events into a room"""
69+
# Mock out create_and_send_nonmember_event to check whether events are being sent
70+
self.event_creation_handler.create_and_send_nonmember_event = Mock(
71+
spec=[],
72+
side_effect=self.event_creation_handler.create_and_send_nonmember_event,
73+
)
74+
75+
# Create a user and room to play with
76+
user_id = self.register_user("summer", "monkey")
77+
tok = self.login("summer", "monkey")
78+
room_id = self.helper.create_room_as(user_id, tok=tok)
79+
80+
# Create and send a non-state event
81+
content = {"body": "I am a puppet", "msgtype": "m.text"}
82+
event_dict = {
83+
"room_id": room_id,
84+
"type": "m.room.message",
85+
"content": content,
86+
"sender": user_id,
87+
}
88+
event = self.get_success(
89+
self.module_api.create_and_send_event_into_room(event_dict)
90+
) # type: EventBase
91+
self.assertEqual(event.sender, user_id)
92+
self.assertEqual(event.type, "m.room.message")
93+
self.assertEqual(event.room_id, room_id)
94+
self.assertFalse(hasattr(event, "state_key"))
95+
self.assertDictEqual(event.content, content)
96+
97+
# Check that the event was sent
98+
self.event_creation_handler.create_and_send_nonmember_event.assert_called_with(
99+
create_requester(user_id),
100+
event_dict,
101+
ratelimit=False,
102+
ignore_shadow_ban=True,
103+
)
104+
105+
# Create and send a state event
106+
content = {
107+
"events_default": 0,
108+
"users": {user_id: 100},
109+
"state_default": 50,
110+
"users_default": 0,
111+
"events": {"test.event.type": 25},
112+
}
113+
event_dict = {
114+
"room_id": room_id,
115+
"type": "m.room.power_levels",
116+
"content": content,
117+
"sender": user_id,
118+
"state_key": "",
119+
}
120+
event = self.get_success(
121+
self.module_api.create_and_send_event_into_room(event_dict)
122+
) # type: EventBase
123+
self.assertEqual(event.sender, user_id)
124+
self.assertEqual(event.type, "m.room.power_levels")
125+
self.assertEqual(event.room_id, room_id)
126+
self.assertEqual(event.state_key, "")
127+
self.assertDictEqual(event.content, content)
128+
129+
# Check that the event was sent
130+
self.event_creation_handler.create_and_send_nonmember_event.assert_called_with(
131+
create_requester(user_id),
132+
{
133+
"type": "m.room.power_levels",
134+
"content": content,
135+
"room_id": room_id,
136+
"sender": user_id,
137+
"state_key": "",
138+
},
139+
ratelimit=False,
140+
ignore_shadow_ban=True,
141+
)
142+
143+
# Check that we can't send membership events
144+
content = {
145+
"membership": "leave",
146+
}
147+
event_dict = {
148+
"room_id": room_id,
149+
"type": "m.room.member",
150+
"content": content,
151+
"sender": user_id,
152+
"state_key": user_id,
153+
}
154+
self.get_failure(
155+
self.module_api.create_and_send_event_into_room(event_dict), Exception
156+
)
157+
63158
def test_public_rooms(self):
64159
"""Tests that a room can be added and removed from the public rooms list,
65160
as well as have its public rooms directory state queried.

tests/rest/client/test_third_party_rules.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
import threading
16+
from typing import Dict
1617

1718
from mock import Mock
1819

1920
from synapse.events import EventBase
21+
from synapse.module_api import ModuleApi
2022
from synapse.rest import admin
2123
from synapse.rest.client.v1 import login, room
2224
from synapse.types import Requester, StateMap
@@ -27,10 +29,11 @@
2729

2830

2931
class ThirdPartyRulesTestModule:
30-
def __init__(self, config, module_api):
32+
def __init__(self, config: Dict, module_api: ModuleApi):
3133
# keep a record of the "current" rules module, so that the test can patch
3234
# it if desired.
3335
thread_local.rules_module = self
36+
self.module_api = module_api
3437

3538
async def on_create_room(
3639
self, requester: Requester, config: dict, is_requester_admin: bool
@@ -142,3 +145,26 @@ async def check(ev: EventBase, state):
142145
self.assertEqual(channel.result["code"], b"200", channel.result)
143146
ev = channel.json_body
144147
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)