0
0
Fork 1
mirror of https://mau.dev/maunium/synapse.git synced 2024-09-21 00:59:00 +02:00

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 is contained in:
Andrew Morgan 2020-10-09 13:46:36 +01:00 committed by GitHub
parent 5009ffcaa4
commit 66ac4b1e34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 157 additions and 8 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

@ -59,6 +59,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__)
@ -393,7 +394,9 @@ class EventCreationHandler:
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
@ -1229,11 +1232,7 @@ class EventCreationHandler:
# Since this is a dummy-event it is OK if it is sent by a # Since this is a dummy-event it is OK if it is sent by a
# shadow-banned user. # shadow-banned user.
await self.handle_new_client_event( await self.handle_new_client_event(
requester=requester, requester, event, context, ratelimit=False, ignore_shadow_ban=True,
event=event,
context=context,
ratelimit=False,
ignore_shadow_ban=True,
) )
return True return True
except ConsentNotGivenError: except ConsentNotGivenError:

View file

@ -18,11 +18,12 @@ from typing import TYPE_CHECKING, Iterable, Optional, Tuple
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.storage.state import StateFilter from synapse.storage.state import StateFilter
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
@ -320,6 +321,33 @@ class ModuleApi:
state = yield defer.ensureDeferred(self._store.get_events(state_ids.values())) state = yield defer.ensureDeferred(self._store.get_events(state_ids.values()))
return state.values() return state.values()
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, ignore_shadow_ban=True,
)
return event
class PublicRoomListManager: class PublicRoomListManager:
"""Contains methods for adding to, removing from and querying whether a room """Contains methods for adding to, removing from and querying whether a room

View file

@ -12,9 +12,12 @@
# 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.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 +32,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 = homeserver.get_module_api() self.module_api = homeserver.get_module_api()
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 +64,97 @@ 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,
ignore_shadow_ban=True,
)
# 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,
ignore_shadow_ban=True,
)
# 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

@ -13,10 +13,12 @@
# 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.
import threading import threading
from typing import Dict
from mock import Mock from mock import Mock
from synapse.events import EventBase from synapse.events import EventBase
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 Requester, StateMap from synapse.types import Requester, StateMap
@ -27,10 +29,11 @@ thread_local = threading.local()
class ThirdPartyRulesTestModule: class ThirdPartyRulesTestModule:
def __init__(self, config, module_api): def __init__(self, config: Dict, module_api: ModuleApi):
# keep a record of the "current" rules module, so that the test can patch # keep a record of the "current" rules module, so that the test can patch
# it if desired. # it if desired.
thread_local.rules_module = self thread_local.rules_module = self
self.module_api = module_api
async def on_create_room( async def on_create_room(
self, requester: Requester, config: dict, is_requester_admin: bool self, requester: Requester, config: dict, is_requester_admin: bool
@ -142,3 +145,26 @@ class ThirdPartyRulesTestCase(unittest.HomeserverTestCase):
self.assertEqual(channel.result["code"], b"200", channel.result) self.assertEqual(channel.result["code"], b"200", channel.result)
ev = channel.json_body ev = channel.json_body
self.assertEqual(ev["content"]["x"], "y") 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)