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

Allow modules to create and send events into rooms #8479

Merged
merged 16 commits into from
Oct 9, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8479.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add the ability to send non-membership events into a room via the `ModuleApi`.
11 changes: 5 additions & 6 deletions synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
from ._base import BaseHandler

if TYPE_CHECKING:
from synapse.events.third_party_rules import ThirdPartyEventRules
from synapse.server import HomeServer

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

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.config.block_events_without_consent_error
Expand Down Expand Up @@ -1229,11 +1232,7 @@ async def _send_dummy_event_for_room(self, room_id: str) -> bool:
# Since this is a dummy-event it is OK if it is sent by a
# shadow-banned user.
await self.handle_new_client_event(
requester=requester,
event=event,
context=context,
ratelimit=False,
ignore_shadow_ban=True,
requester, event, context, ratelimit=False, ignore_shadow_ban=True,
)
return True
except ConsentNotGivenError:
Expand Down
30 changes: 29 additions & 1 deletion synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@

from twisted.internet import defer

from synapse.events import EventBase
from synapse.http.client import SimpleHttpClient
from synapse.http.site import SynapseRequest
from synapse.logging.context import make_deferred_yieldable, run_in_background
from synapse.storage.state import StateFilter
from synapse.types import UserID
from synapse.types import JsonDict, UserID, create_requester

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -320,6 +321,33 @@ def get_state_events_in_room(
state = yield defer.ensureDeferred(self._store.get_events(state_ids.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:
"""Contains methods for adding to, removing from and querying whether a room
Expand Down
95 changes: 95 additions & 0 deletions tests/module_api/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@
# 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 mock import Mock

from synapse.events import EventBase
from synapse.rest import admin
from synapse.rest.client.v1 import login, room
from synapse.types import create_requester

from tests.unittest import HomeserverTestCase

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

def test_can_register_user(self):
"""Tests that an external module can register a user"""
Expand Down Expand Up @@ -60,6 +64,97 @@ def test_can_register_user(self):
displayname = self.get_success(self.store.get_profile_displayname("bob"))
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 = self.get_success(
self.module_api.create_and_send_event_into_room(
user_id, room_id, event_type="m.room.message", content=content,
)
) # 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),
{
"type": "m.room.message",
"content": content,
"room_id": room_id,
"sender": user_id,
},
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 = self.get_success(
self.module_api.create_and_send_event_into_room(
user_id,
room_id,
event_type="m.room.power_levels",
content=content,
state_key="",
)
) # 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",
}
self.get_failure(
self.module_api.create_and_send_event_into_room(
user_id,
room_id,
event_type="m.room.message",
content=content,
state_key=user_id,
),
Exception,
)

def test_public_rooms(self):
"""Tests that a room can be added and removed from the public rooms list,
as well as have its public rooms directory state queried.
Expand Down
22 changes: 21 additions & 1 deletion tests/rest/client/test_third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
# 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
Expand All @@ -27,10 +29,11 @@


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
# 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
Expand Down Expand Up @@ -142,3 +145,20 @@ async def check(ev: EventBase, state):
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 = self.get_success(
current_rules_module().module_api.create_and_send_event_into_room(
self.user_id, self.room_id, "m.room.message", content,
)
) # 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)