1
0

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.
This commit is contained in:
Andrew Morgan
2020-10-09 13:46:36 +01:00
committed by Andrew Morgan
parent 11523b507b
commit 62c7b10ea5
7 changed files with 299 additions and 89 deletions

1
changelog.d/8479.feature Normal file
View File

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

View File

@@ -124,7 +124,9 @@ class ThirdPartyEventRules(object):
if self.third_party_rules is None: if self.third_party_rules is None:
return True return True
check_func = getattr(self.third_party_rules, "check_visibility_can_be_modified") check_func = getattr(
self.third_party_rules, "check_visibility_can_be_modified", None
)
if not check_func or not isinstance(check_func, Callable): if not check_func or not isinstance(check_func, Callable):
return True return True

View File

@@ -63,6 +63,7 @@ from synapse.visibility import filter_events_for_client
from ._base import BaseHandler from ._base import BaseHandler
if TYPE_CHECKING: if TYPE_CHECKING:
from synapse.events.third_party_rules import ThirdPartyEventRules
from synapse.server import HomeServer from synapse.server import HomeServer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -396,7 +397,9 @@ class EventCreationHandler(object):
self.action_generator = hs.get_action_generator() self.action_generator = hs.get_action_generator()
self.spam_checker = hs.get_spam_checker() self.spam_checker = hs.get_spam_checker()
self.third_party_event_rules = hs.get_third_party_event_rules() self.third_party_event_rules = (
self.hs.get_third_party_event_rules()
) # type: ThirdPartyEventRules
self._block_events_without_consent_error = ( self._block_events_without_consent_error = (
self.config.block_events_without_consent_error self.config.block_events_without_consent_error

View File

@@ -18,10 +18,11 @@ from typing import TYPE_CHECKING
from twisted.internet import defer from twisted.internet import defer
from synapse.events import EventBase
from synapse.http.client import SimpleHttpClient from synapse.http.client import SimpleHttpClient
from synapse.http.site import SynapseRequest from synapse.http.site import SynapseRequest
from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.logging.context import make_deferred_yieldable, run_in_background
from synapse.types import UserID from synapse.types import JsonDict, UserID, create_requester
if TYPE_CHECKING: if TYPE_CHECKING:
from synapse.server import HomeServer from synapse.server import HomeServer
@@ -310,3 +311,30 @@ class ModuleApi(object):
await self._auth_handler.complete_sso_login( await self._auth_handler.complete_sso_login(
registered_user_id, request, client_redirect_url, registered_user_id, request, client_redirect_url,
) )
async def create_and_send_event_into_room(self, event_dict: JsonDict) -> EventBase:
"""Create and send an event into a room. Membership events are currently not supported.
Args:
event_dict: A dictionary representing the event to send.
Required keys are `type`, `room_id`, `sender` and `content`.
Returns:
The event that was sent. If state event deduplication happened, then
the previous, duplicate event instead.
Raises:
SynapseError if the event was not allowed.
"""
# Create a requester object
requester = create_requester(event_dict["sender"])
# Create and send the event
(
event,
_,
) = await self._hs.get_event_creation_handler().create_and_send_nonmember_event(
requester, event_dict, ratelimit=False
)
return event

View File

@@ -12,9 +12,13 @@
# 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 mock import Mock
from synapse.events import EventBase
from synapse.module_api import ModuleApi from synapse.module_api import ModuleApi
from synapse.rest import admin from synapse.rest import admin
from synapse.rest.client.v1 import login, room from synapse.rest.client.v1 import login, room
from synapse.types import create_requester
from tests.unittest import HomeserverTestCase from tests.unittest import HomeserverTestCase
@@ -29,6 +33,7 @@ class ModuleApiTestCase(HomeserverTestCase):
def prepare(self, reactor, clock, homeserver): def prepare(self, reactor, clock, homeserver):
self.store = homeserver.get_datastore() self.store = homeserver.get_datastore()
self.module_api = ModuleApi(homeserver, homeserver.get_auth_handler()) self.module_api = ModuleApi(homeserver, homeserver.get_auth_handler())
self.event_creation_handler = homeserver.get_event_creation_handler()
def test_can_register_user(self): def test_can_register_user(self):
"""Tests that an external module can register a user""" """Tests that an external module can register a user"""
@@ -60,6 +65,93 @@ class ModuleApiTestCase(HomeserverTestCase):
displayname = self.get_success(self.store.get_profile_displayname("bob")) displayname = self.get_success(self.store.get_profile_displayname("bob"))
self.assertEqual(displayname, "Bobberino") self.assertEqual(displayname, "Bobberino")
def test_sending_events_into_room(self):
"""Tests that a module can send events into a room"""
# Mock out create_and_send_nonmember_event to check whether events are being sent
self.event_creation_handler.create_and_send_nonmember_event = Mock(
spec=[],
side_effect=self.event_creation_handler.create_and_send_nonmember_event,
)
# Create a user and room to play with
user_id = self.register_user("summer", "monkey")
tok = self.login("summer", "monkey")
room_id = self.helper.create_room_as(user_id, tok=tok)
# Create and send a non-state event
content = {"body": "I am a puppet", "msgtype": "m.text"}
event_dict = {
"room_id": room_id,
"type": "m.room.message",
"content": content,
"sender": user_id,
}
event = self.get_success(
self.module_api.create_and_send_event_into_room(event_dict)
) # type: EventBase
self.assertEqual(event.sender, user_id)
self.assertEqual(event.type, "m.room.message")
self.assertEqual(event.room_id, room_id)
self.assertFalse(hasattr(event, "state_key"))
self.assertDictEqual(event.content, content)
# Check that the event was sent
self.event_creation_handler.create_and_send_nonmember_event.assert_called_with(
create_requester(user_id), event_dict, ratelimit=False,
)
# Create and send a state event
content = {
"events_default": 0,
"users": {user_id: 100},
"state_default": 50,
"users_default": 0,
"events": {"test.event.type": 25},
}
event_dict = {
"room_id": room_id,
"type": "m.room.power_levels",
"content": content,
"sender": user_id,
"state_key": "",
}
event = self.get_success(
self.module_api.create_and_send_event_into_room(event_dict)
) # type: EventBase
self.assertEqual(event.sender, user_id)
self.assertEqual(event.type, "m.room.power_levels")
self.assertEqual(event.room_id, room_id)
self.assertEqual(event.state_key, "")
self.assertDictEqual(event.content, content)
# Check that the event was sent
self.event_creation_handler.create_and_send_nonmember_event.assert_called_with(
create_requester(user_id),
{
"type": "m.room.power_levels",
"content": content,
"room_id": room_id,
"sender": user_id,
"state_key": "",
},
ratelimit=False,
)
# Check that we can't send membership events
content = {
"membership": "leave",
}
event_dict = {
"room_id": room_id,
"type": "m.room.member",
"content": content,
"sender": user_id,
"state_key": user_id,
}
self.get_failure(
self.module_api.create_and_send_event_into_room(event_dict), Exception
)
def test_public_rooms(self): def test_public_rooms(self):
"""Tests that a room can be added and removed from the public rooms list, """Tests that a room can be added and removed from the public rooms list,
as well as have its public rooms directory state queried. as well as have its public rooms directory state queried.

View File

@@ -0,0 +1,170 @@
# -*- coding: utf-8 -*-
# Copyright 2019 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import threading
from typing import Dict
from mock import Mock
from synapse.events import EventBase
from synapse.module_api import ModuleApi
from synapse.rest import admin
from synapse.rest.client.v1 import login, room
from synapse.types import Requester, StateMap
from tests import unittest
thread_local = threading.local()
class ThirdPartyRulesTestModule:
def __init__(self, config: Dict, module_api: ModuleApi):
# keep a record of the "current" rules module, so that the test can patch
# it if desired.
thread_local.rules_module = self
self.module_api = module_api
async def on_create_room(
self, requester: Requester, config: dict, is_requester_admin: bool
):
return True
async def check_event_allowed(self, event: EventBase, state: StateMap[EventBase]):
return True
@staticmethod
def parse_config(config):
return config
def current_rules_module() -> ThirdPartyRulesTestModule:
return thread_local.rules_module
class ThirdPartyRulesTestCase(unittest.HomeserverTestCase):
servlets = [
admin.register_servlets,
login.register_servlets,
room.register_servlets,
]
def default_config(self):
config = super().default_config()
config["third_party_event_rules"] = {
"module": __name__ + ".ThirdPartyRulesTestModule",
"config": {},
}
return config
def prepare(self, reactor, clock, homeserver):
# Create a user and room to play with during the tests
self.user_id = self.register_user("kermit", "monkey")
self.tok = self.login("kermit", "monkey")
self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok)
def test_third_party_rules(self):
"""Tests that a forbidden event is forbidden from being sent, but an allowed one
can be sent.
"""
# patch the rules module with a Mock which will return False for some event
# types
async def check(ev, state):
return ev.type != "foo.bar.forbidden"
callback = Mock(spec=[], side_effect=check)
current_rules_module().check_event_allowed = callback
request, channel = self.make_request(
"PUT",
"/_matrix/client/r0/rooms/%s/send/foo.bar.allowed/1" % self.room_id,
{},
access_token=self.tok,
)
self.render(request)
self.assertEquals(channel.result["code"], b"200", channel.result)
callback.assert_called_once()
# there should be various state events in the state arg: do some basic checks
state_arg = callback.call_args[0][1]
for k in (("m.room.create", ""), ("m.room.member", self.user_id)):
self.assertIn(k, state_arg)
ev = state_arg[k]
self.assertEqual(ev.type, k[0])
self.assertEqual(ev.state_key, k[1])
request, channel = self.make_request(
"PUT",
"/_matrix/client/r0/rooms/%s/send/foo.bar.forbidden/1" % self.room_id,
{},
access_token=self.tok,
)
self.render(request)
self.assertEquals(channel.result["code"], b"403", channel.result)
def test_modify_event(self):
"""Tests that the module can successfully tweak an event before it is persisted.
"""
# first patch the event checker so that it will modify the event
async def check(ev: EventBase, state):
ev.content = {"x": "y"}
return True
current_rules_module().check_event_allowed = check
# now send the event
request, channel = self.make_request(
"PUT",
"/_matrix/client/r0/rooms/%s/send/modifyme/1" % self.room_id,
{"x": "x"},
access_token=self.tok,
)
self.render(request)
self.assertEqual(channel.result["code"], b"200", channel.result)
event_id = channel.json_body["event_id"]
# ... and check that it got modified
request, channel = self.make_request(
"GET",
"/_matrix/client/r0/rooms/%s/event/%s" % (self.room_id, event_id),
access_token=self.tok,
)
self.render(request)
self.assertEqual(channel.result["code"], b"200", channel.result)
ev = channel.json_body
self.assertEqual(ev["content"]["x"], "y")
def test_send_event(self):
"""Tests that the module can send an event into a room via the module api"""
content = {
"msgtype": "m.text",
"body": "Hello!",
}
event_dict = {
"room_id": self.room_id,
"type": "m.room.message",
"content": content,
"sender": self.user_id,
}
event = self.get_success(
current_rules_module().module_api.create_and_send_event_into_room(
event_dict
)
) # type: EventBase
self.assertEquals(event.sender, self.user_id)
self.assertEquals(event.room_id, self.room_id)
self.assertEquals(event.type, "m.room.message")
self.assertEquals(event.content, content)

View File

@@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright 2019 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.rest import admin
from synapse.rest.client.v1 import login, room
from synapse.types import Requester
from tests import unittest
class ThirdPartyRulesTestModule(object):
def __init__(self, config, *args, **kwargs):
pass
async def on_create_room(
self, requester: Requester, config: dict, is_requester_admin: bool
):
return True
async def check_event_allowed(self, event, context):
if event.type == "foo.bar.forbidden":
return False
else:
return True
@staticmethod
def parse_config(config):
return config
class ThirdPartyRulesTestCase(unittest.HomeserverTestCase):
servlets = [
admin.register_servlets,
login.register_servlets,
room.register_servlets,
]
def make_homeserver(self, reactor, clock):
config = self.default_config()
config["third_party_event_rules"] = {
"module": "tests.rest.client.third_party_rules.ThirdPartyRulesTestModule",
"config": {},
}
self.hs = self.setup_test_homeserver(config=config)
return self.hs
def prepare(self, reactor, clock, homeserver):
# Create a user and room to play with during the tests
self.user_id = self.register_user("kermit", "monkey")
self.tok = self.login("kermit", "monkey")
self.room_id = self.helper.create_room_as(self.user_id, tok=self.tok)
def test_third_party_rules(self):
"""Tests that a forbidden event is forbidden from being sent, but an allowed one
can be sent.
"""
request, channel = self.make_request(
"PUT",
"/_matrix/client/r0/rooms/%s/send/foo.bar.allowed/1" % self.room_id,
{},
access_token=self.tok,
)
self.render(request)
self.assertEquals(channel.result["code"], b"200", channel.result)
request, channel = self.make_request(
"PUT",
"/_matrix/client/r0/rooms/%s/send/foo.bar.forbidden/1" % self.room_id,
{},
access_token=self.tok,
)
self.render(request)
self.assertEquals(channel.result["code"], b"403", channel.result)