Add a ModuleApi method to update a user's membership in a room (#11147)

Co-authored-by: reivilibre <oliverw@matrix.org>
This commit is contained in:
Brendan Abolivier 2021-10-28 18:45:53 +02:00 committed by GitHub
parent 1bfd141205
commit adc0d35b17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 225 additions and 2 deletions

View File

@ -0,0 +1 @@
Add a module API method to update a user's membership in a room.

View File

@ -33,6 +33,7 @@ import jinja2
from twisted.internet import defer from twisted.internet import defer
from twisted.web.resource import IResource from twisted.web.resource import IResource
from synapse.api.errors import SynapseError
from synapse.events import EventBase from synapse.events import EventBase
from synapse.events.presence_router import PresenceRouter from synapse.events.presence_router import PresenceRouter
from synapse.http.client import SimpleHttpClient from synapse.http.client import SimpleHttpClient
@ -625,8 +626,105 @@ 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 update_room_membership(
self,
sender: str,
target: str,
room_id: str,
new_membership: str,
content: Optional[JsonDict] = None,
) -> EventBase:
"""Updates the membership of a user to the given value.
Added in Synapse v1.46.0.
Args:
sender: The user performing the membership change. Must be a user local to
this homeserver.
target: The user whose membership is changing. This is often the same value
as `sender`, but it might differ in some cases (e.g. when kicking a user,
the `sender` is the user performing the kick and the `target` is the user
being kicked).
room_id: The room in which to change the membership.
new_membership: The new membership state of `target` after this operation. See
https://spec.matrix.org/unstable/client-server-api/#mroommember for the
list of allowed values.
content: Additional values to include in the resulting event's content.
Returns:
The newly created membership event.
Raises:
RuntimeError if the `sender` isn't a local user.
ShadowBanError if a shadow-banned requester attempts to send an invite.
SynapseError if the module attempts to send a membership event that isn't
allowed, either by the server's configuration (e.g. trying to set a
per-room display name that's too long) or by the validation rules around
membership updates (e.g. the `membership` value is invalid).
"""
if not self.is_mine(sender):
raise RuntimeError(
"Tried to send an event as a user that isn't local to this homeserver",
)
requester = create_requester(sender)
target_user_id = UserID.from_string(target)
if content is None:
content = {}
# Set the profile if not already done by the module.
if "avatar_url" not in content or "displayname" not in content:
try:
# Try to fetch the user's profile.
profile = await self._hs.get_profile_handler().get_profile(
target_user_id.to_string(),
)
except SynapseError as e:
# If the profile couldn't be found, use default values.
profile = {
"displayname": target_user_id.localpart,
"avatar_url": None,
}
if e.code != 404:
# If the error isn't 404, it means we tried to fetch the profile over
# federation but the remote server responded with a non-standard
# status code.
logger.error(
"Got non-404 error status when fetching profile for %s",
target_user_id.to_string(),
)
# Set the profile where it needs to be set.
if "avatar_url" not in content:
content["avatar_url"] = profile["avatar_url"]
if "displayname" not in content:
content["displayname"] = profile["displayname"]
event_id, _ = await self._hs.get_room_member_handler().update_membership(
requester=requester,
target=target_user_id,
room_id=room_id,
action=new_membership,
content=content,
)
# Try to retrieve the resulting event.
event = await self._hs.get_datastore().get_event(event_id)
# update_membership is supposed to always return after the event has been
# successfully persisted.
assert event is not None
return event
async def create_and_send_event_into_room(self, event_dict: JsonDict) -> EventBase: 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. """Create and send an event into a room.
Membership events are not supported by this method. To update a user's membership
in a room, please use the `update_room_membership` method instead.
Added in Synapse v1.22.0. Added in Synapse v1.22.0.

View File

@ -20,7 +20,7 @@ from synapse.events import EventBase
from synapse.federation.units import Transaction from synapse.federation.units import Transaction
from synapse.handlers.presence import UserPresenceState from synapse.handlers.presence import UserPresenceState
from synapse.rest import admin from synapse.rest import admin
from synapse.rest.client import login, presence, room from synapse.rest.client import login, presence, profile, room
from synapse.types import create_requester from synapse.types import create_requester
from tests.events.test_presence_router import send_presence_update, sync_presence from tests.events.test_presence_router import send_presence_update, sync_presence
@ -37,6 +37,7 @@ class ModuleApiTestCase(HomeserverTestCase):
login.register_servlets, login.register_servlets,
room.register_servlets, room.register_servlets,
presence.register_servlets, presence.register_servlets,
profile.register_servlets,
] ]
def prepare(self, reactor, clock, homeserver): def prepare(self, reactor, clock, homeserver):
@ -385,6 +386,129 @@ class ModuleApiTestCase(HomeserverTestCase):
self.assertTrue(found_update) self.assertTrue(found_update)
def test_update_membership(self):
"""Tests that the module API can update the membership of a user in a room."""
peter = self.register_user("peter", "hackme")
lesley = self.register_user("lesley", "hackme")
tok = self.login("peter", "hackme")
lesley_tok = self.login("lesley", "hackme")
# Make peter create a public room.
room_id = self.helper.create_room_as(
room_creator=peter, is_public=True, tok=tok
)
# Set a profile for lesley.
channel = self.make_request(
method="PUT",
path="/_matrix/client/r0/profile/%s/displayname" % lesley,
content={"displayname": "Lesley May"},
access_token=lesley_tok,
)
self.assertEqual(channel.code, 200, channel.result)
channel = self.make_request(
method="PUT",
path="/_matrix/client/r0/profile/%s/avatar_url" % lesley,
content={"avatar_url": "some_url"},
access_token=lesley_tok,
)
self.assertEqual(channel.code, 200, channel.result)
# Make Peter invite Lesley to the room.
self.get_success(
defer.ensureDeferred(
self.module_api.update_room_membership(peter, lesley, room_id, "invite")
)
)
res = self.helper.get_state(
room_id=room_id,
event_type="m.room.member",
state_key=lesley,
tok=tok,
)
# Check the membership is correct.
self.assertEqual(res["membership"], "invite")
# Also check that the profile was correctly filled out, and that it's not
# Peter's.
self.assertEqual(res["displayname"], "Lesley May")
self.assertEqual(res["avatar_url"], "some_url")
# Make lesley join it.
self.get_success(
defer.ensureDeferred(
self.module_api.update_room_membership(lesley, lesley, room_id, "join")
)
)
# Check that the membership of lesley in the room is "join".
res = self.helper.get_state(
room_id=room_id,
event_type="m.room.member",
state_key=lesley,
tok=tok,
)
self.assertEqual(res["membership"], "join")
# Also check that the profile was correctly filled out.
self.assertEqual(res["displayname"], "Lesley May")
self.assertEqual(res["avatar_url"], "some_url")
# Make peter kick lesley from the room.
self.get_success(
defer.ensureDeferred(
self.module_api.update_room_membership(peter, lesley, room_id, "leave")
)
)
# Check that the membership of lesley in the room is "leave".
res = self.helper.get_state(
room_id=room_id,
event_type="m.room.member",
state_key=lesley,
tok=tok,
)
self.assertEqual(res["membership"], "leave")
# Try to send a membership update from a non-local user and check that it fails.
d = defer.ensureDeferred(
self.module_api.update_room_membership(
"@nicolas:otherserver.com",
lesley,
room_id,
"invite",
)
)
self.get_failure(d, RuntimeError)
# Check that inviting a user that doesn't have a profile falls back to using a
# default (localpart + no avatar) profile.
simone = "@simone:" + self.hs.config.server.server_name
self.get_success(
defer.ensureDeferred(
self.module_api.update_room_membership(peter, simone, room_id, "invite")
)
)
res = self.helper.get_state(
room_id=room_id,
event_type="m.room.member",
state_key=simone,
tok=tok,
)
self.assertEqual(res["membership"], "invite")
self.assertEqual(res["displayname"], "simone")
self.assertIsNone(res["avatar_url"])
class ModuleApiWorkerTestCase(BaseMultiWorkerStreamTestCase): class ModuleApiWorkerTestCase(BaseMultiWorkerStreamTestCase):
"""For testing ModuleApi functionality in a multi-worker setup""" """For testing ModuleApi functionality in a multi-worker setup"""