mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-12-25 21:29:25 -05:00
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:
parent
1bfd141205
commit
adc0d35b17
1
changelog.d/11147.feature
Normal file
1
changelog.d/11147.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add a module API method to update a user's membership in a room.
|
@ -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.
|
||||||
|
|
||||||
|
@ -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"""
|
||||||
|
Loading…
Reference in New Issue
Block a user