mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-28 00:44:35 +00:00
Merge branches 'matthew/msc4233-knock-via' and 'matthew/auto-join-on-knock-accept' into matthew/knock-push-rules
This commit is contained in:
@@ -36,7 +36,7 @@ from synapse.events.auto_accept_invites import InviteAutoAccepter
|
||||
from synapse.handlers.sync import JoinedSyncResult, SyncRequestKey
|
||||
from synapse.module_api import ModuleApi
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import login, room
|
||||
from synapse.rest.client import knock, login, room
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import StreamToken, UserID, UserInfo, create_requester
|
||||
from synapse.util.clock import Clock
|
||||
@@ -58,10 +58,114 @@ class AutoAcceptInvitesTestCase(FederatingHomeserverTestCase):
|
||||
|
||||
servlets = [
|
||||
admin.register_servlets,
|
||||
knock.register_servlets,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
]
|
||||
|
||||
def _create_knockable_room_with_pending_knock(
|
||||
self,
|
||||
) -> tuple[str, str, str, str]:
|
||||
"""Create a knockable room and have a second local user knock on it.
|
||||
|
||||
Returns a tuple of (room_id, creator_id, creator_tok, knocker_id).
|
||||
"""
|
||||
creator_id = self.register_user("creator", "pass")
|
||||
creator_tok = self.login("creator", "pass")
|
||||
|
||||
knocker_id = self.register_user("knocker", "pass")
|
||||
knocker_tok = self.login("knocker", "pass")
|
||||
|
||||
room_id = self.helper.create_room_as(
|
||||
creator_id,
|
||||
is_public=False,
|
||||
tok=creator_tok,
|
||||
)
|
||||
self.helper.send_state(
|
||||
room_id,
|
||||
EventTypes.JoinRules,
|
||||
{"join_rule": "knock"},
|
||||
tok=creator_tok,
|
||||
)
|
||||
|
||||
self.helper.knock(room=room_id, user=knocker_id, tok=knocker_tok)
|
||||
|
||||
return room_id, creator_id, creator_tok, knocker_id
|
||||
|
||||
def test_auto_join_on_accepted_knock(self) -> None:
|
||||
"""A user whose knock is accepted (invited by a room member) is
|
||||
automatically joined to the room, even with no `auto_accept_invites`
|
||||
configuration at all."""
|
||||
(
|
||||
room_id,
|
||||
creator_id,
|
||||
creator_tok,
|
||||
knocker_id,
|
||||
) = self._create_knockable_room_with_pending_knock()
|
||||
|
||||
# The creator accepts the knock by inviting the knocker.
|
||||
self.helper.invite(
|
||||
room_id,
|
||||
creator_id,
|
||||
knocker_id,
|
||||
tok=creator_tok,
|
||||
)
|
||||
|
||||
# The knocker is automatically joined to the room.
|
||||
join_updates, _ = sync_join(self, knocker_id)
|
||||
self.assertEqual(len(join_updates), 1)
|
||||
self.assertEqual(join_updates[0].room_id, room_id)
|
||||
|
||||
@override_config(
|
||||
{
|
||||
"auto_accept_invites": {
|
||||
"enabled_for_accepted_knocks": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
def test_no_auto_join_on_accepted_knock_when_disabled(self) -> None:
|
||||
"""With `enabled_for_accepted_knocks` off, an accepted knock stays an
|
||||
ordinary invite."""
|
||||
(
|
||||
room_id,
|
||||
creator_id,
|
||||
creator_tok,
|
||||
knocker_id,
|
||||
) = self._create_knockable_room_with_pending_knock()
|
||||
|
||||
self.helper.invite(
|
||||
room_id,
|
||||
creator_id,
|
||||
knocker_id,
|
||||
tok=creator_tok,
|
||||
)
|
||||
|
||||
join_updates, _ = sync_join(self, knocker_id)
|
||||
self.assertEqual(len(join_updates), 0)
|
||||
|
||||
def test_plain_invite_not_auto_accepted_by_default(self) -> None:
|
||||
"""A plain invite (no prior knock) is not auto-accepted just because
|
||||
the accepted-knocks logic is enabled by default."""
|
||||
inviting_user_id = self.register_user("inviter2", "pass")
|
||||
inviting_user_tok = self.login("inviter2", "pass")
|
||||
|
||||
invited_user_id = self.register_user("invitee2", "pass")
|
||||
self.login("invitee2", "pass")
|
||||
|
||||
room_id = self.helper.create_room_as(
|
||||
inviting_user_id, is_public=False, tok=inviting_user_tok
|
||||
)
|
||||
|
||||
self.helper.invite(
|
||||
room_id,
|
||||
inviting_user_id,
|
||||
invited_user_id,
|
||||
tok=inviting_user_tok,
|
||||
)
|
||||
|
||||
join_updates, _ = sync_join(self, invited_user_id)
|
||||
self.assertEqual(len(join_updates), 0)
|
||||
|
||||
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
|
||||
hs = self.setup_test_homeserver()
|
||||
self.handler = hs.get_federation_handler()
|
||||
@@ -561,7 +665,12 @@ class InviteAutoAccepterInternalTestCase(TestCase):
|
||||
"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.module = create_module()
|
||||
# These tests exercise the plain-invite acceptance path, which is
|
||||
# gated on `enabled` in `on_new_event` (the accepted-knock path is
|
||||
# what's on by default).
|
||||
self.module = create_module(
|
||||
config_override={"auto_accept_invites": {"enabled": True}}
|
||||
)
|
||||
self.user_id = "@peter:test"
|
||||
self.invitee = "@lesley:test"
|
||||
self.remote_invitee = "@thomas:remote"
|
||||
@@ -772,6 +881,11 @@ class MockEvent:
|
||||
"""Checks if the event is a state event by checking if it has a state key."""
|
||||
return self.state_key is not None
|
||||
|
||||
def auth_event_ids(self) -> list[str]:
|
||||
"""The module walks the auth events when checking whether an invite
|
||||
accepts a knock; a mocked event has none."""
|
||||
return []
|
||||
|
||||
@property
|
||||
def membership(self) -> str:
|
||||
"""Extracts the membership from the event. Should only be called on an event
|
||||
@@ -800,6 +914,12 @@ def create_module(
|
||||
module_api = Mock(spec=ModuleApi)
|
||||
module_api.is_mine.side_effect = lambda a: a.split(":")[1] == "test"
|
||||
module_api.worker_name = worker_name
|
||||
# The module reaches into the datastore to walk an invite's auth events;
|
||||
# none of the mocked events have any.
|
||||
module_api._store = Mock()
|
||||
module_api._store.get_event.side_effect = lambda *_args, **_kwargs: (
|
||||
make_awaitable(None)
|
||||
)
|
||||
module_api.sleep.return_value = lambda *_args, **_kwargs: make_awaitable(None)
|
||||
module_api.get_userinfo_by_id.return_value = UserInfo(
|
||||
user_id=UserID.from_string("@user:test"),
|
||||
|
||||
@@ -28,14 +28,13 @@ from twisted.internet.testing import MemoryReactor
|
||||
|
||||
from synapse.api.constants import EventContentFields, EventTypes, Membership
|
||||
from synapse.api.room_versions import RoomVersion, RoomVersions
|
||||
from synapse.events import EventBase
|
||||
from synapse.events import builder
|
||||
from synapse.events import EventBase, builder
|
||||
from synapse.events.snapshot import EventContext
|
||||
from synapse.events.utils import strip_event
|
||||
from synapse.http.matrixfederationclient import ByteParser
|
||||
from synapse.http.types import QueryParams
|
||||
from synapse.rest import admin
|
||||
from synapse.rest.client import knock, login, room
|
||||
from synapse.rest.client import knock, login, room, sync
|
||||
from synapse.server import HomeServer
|
||||
from synapse.types import JsonDict
|
||||
from synapse.util.clock import Clock
|
||||
@@ -75,11 +74,18 @@ class KnockViaServerTestCase(unittest.FederatingHomeserverTestCase):
|
||||
knock.register_servlets,
|
||||
room.register_servlets,
|
||||
login.register_servlets,
|
||||
sync.register_servlets,
|
||||
]
|
||||
|
||||
def default_config(self) -> JsonDict:
|
||||
conf = super().default_config()
|
||||
conf["experimental_features"] = {"msc4233_enabled": True}
|
||||
conf["experimental_features"] = {
|
||||
"msc4233_enabled": True,
|
||||
# For the state_after test: out-of-band leaves must be reflected
|
||||
# in `state_after` for clients that no longer apply timeline
|
||||
# events to state.
|
||||
"msc4222_enabled": True,
|
||||
}
|
||||
return conf
|
||||
|
||||
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
|
||||
@@ -417,6 +423,60 @@ class KnockViaServerTestCase(unittest.FederatingHomeserverTestCase):
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
def test_denied_knock_in_state_after(self) -> None:
|
||||
"""The out-of-band leave retracting the knock is reflected in
|
||||
`state_after` (MSC4222) on sync: it is an outlier, so it never enters
|
||||
the current state delta stream, and clients using `state_after` do
|
||||
not apply timeline events to state."""
|
||||
knock_result = self._knock_on_remote_room()
|
||||
|
||||
# Sync up to just after the knock.
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
"/_matrix/client/v3/sync?timeout=0&org.matrix.msc4222.use_state_after=true",
|
||||
access_token=knock_result.local_user1_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
|
||||
since_token = channel.json_body["next_batch"]
|
||||
|
||||
self._send_denial_over_federation(
|
||||
knock_result,
|
||||
auth_events=[
|
||||
knock_result.room_create_event.event_id,
|
||||
knock_result.knock_event_id,
|
||||
],
|
||||
)
|
||||
|
||||
with test_timeout(3, "Denial of the knock was not processed"):
|
||||
while True:
|
||||
membership, _ = self.get_success(
|
||||
self.store.get_local_current_membership_for_user_in_room(
|
||||
knock_result.local_user1_id, knock_result.remote_room_id
|
||||
)
|
||||
)
|
||||
if membership == Membership.LEAVE:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
channel = self.make_request(
|
||||
"GET",
|
||||
f"/_matrix/client/v3/sync?timeout=0&org.matrix.msc4222.use_state_after=true&since={since_token}",
|
||||
access_token=knock_result.local_user1_tok,
|
||||
)
|
||||
self.assertEqual(channel.code, HTTPStatus.OK, channel.json_body)
|
||||
|
||||
leave_room = channel.json_body["rooms"]["leave"][knock_result.remote_room_id]
|
||||
state_after_events = leave_room["org.matrix.msc4222.state_after"]["events"]
|
||||
self.assertTrue(
|
||||
any(
|
||||
e["type"] == EventTypes.Member
|
||||
and e["state_key"] == knock_result.local_user1_id
|
||||
and e["content"]["membership"] == Membership.LEAVE
|
||||
for e in state_after_events
|
||||
),
|
||||
leave_room,
|
||||
)
|
||||
|
||||
def test_denied_knock_ignored_without_knock_in_auth_events(self) -> None:
|
||||
"""A leave event for our knocked user which does not reference the
|
||||
knock in its auth events is ignored."""
|
||||
@@ -495,7 +555,7 @@ class DenyKnockFederationSendTestCase(unittest.FederatingHomeserverTestCase):
|
||||
def test_deny_sends_leave_to_knocking_server(self) -> None:
|
||||
"""Kicking a remote user whose membership is knock sends the leave
|
||||
event to their (otherwise uninvolved) server."""
|
||||
user_id = self.register_user("u1", "pass")
|
||||
self.register_user("u1", "pass")
|
||||
user_token = self.login("u1", "pass")
|
||||
|
||||
fake_knocking_user_id = f"@user:{self.OTHER_SERVER_NAME}"
|
||||
@@ -585,9 +645,7 @@ class DenyKnockFederationSendTestCase(unittest.FederatingHomeserverTestCase):
|
||||
while True:
|
||||
leave_pdus = [
|
||||
pdu
|
||||
for pdu in sent_pdus_by_destination.get(
|
||||
self.OTHER_SERVER_NAME, []
|
||||
)
|
||||
for pdu in sent_pdus_by_destination.get(self.OTHER_SERVER_NAME, [])
|
||||
if pdu.get("type") == EventTypes.Member
|
||||
and pdu.get("state_key") == fake_knocking_user_id
|
||||
and pdu.get("content", {}).get("membership") == Membership.LEAVE
|
||||
|
||||
+4
-1
@@ -1465,7 +1465,10 @@ def start_test_homeserver(
|
||||
for module, module_config in hs.config.modules.loaded_modules:
|
||||
module(config=module_config, api=module_api)
|
||||
|
||||
if hs.config.auto_accept_invites.enabled:
|
||||
if (
|
||||
hs.config.auto_accept_invites.enabled
|
||||
or hs.config.auto_accept_invites.enabled_for_accepted_knocks
|
||||
):
|
||||
# Start the local auto_accept_invites module.
|
||||
m = InviteAutoAccepter(hs.config.auto_accept_invites, module_api)
|
||||
logger.info("Loaded local module %s", m)
|
||||
|
||||
Reference in New Issue
Block a user