diff --git a/synapse/handlers/federation_event.py b/synapse/handlers/federation_event.py index 2ba321eacd..d0556f11e6 100644 --- a/synapse/handlers/federation_event.py +++ b/synapse/handlers/federation_event.py @@ -323,6 +323,18 @@ class FederationEventHandler: pdu.state_key, pdu.room_id ) if origin == via_server or origin == get_domain_from_id(pdu.sender): + # As an outlier the event will never gain a + # `replaces_state`, so reflect the replaced knock in + # `unsigned` ourselves: clients use `prev_content` to + # distinguish a denied knock from a plain kick. + knock_event = await self._store.get_event( + membership_event_id, allow_none=True + ) + if knock_event is not None: + pdu.unsigned["replaces_state"] = knock_event.event_id + pdu.unsigned["prev_content"] = knock_event.content + pdu.unsigned["prev_sender"] = knock_event.sender + # Handle the denial event pdu.internal_metadata.outlier = True pdu.internal_metadata.out_of_band_membership = True diff --git a/tests/federation/test_federation_knock_msc4233.py b/tests/federation/test_federation_knock_msc4233.py index 4f01fd1d05..6900d87031 100644 --- a/tests/federation/test_federation_knock_msc4233.py +++ b/tests/federation/test_federation_knock_msc4233.py @@ -405,9 +405,10 @@ class KnockViaServerTestCase(unittest.FederatingHomeserverTestCase): # The knock should (eventually - the PDU is processed in the # background) be retracted. + leave_event_id = None with test_timeout(3, "Denial of the knock was not processed"): while True: - membership, _ = self.get_success( + membership, leave_event_id = self.get_success( self.store.get_local_current_membership_for_user_in_room( knock_result.local_user1_id, knock_result.remote_room_id ) @@ -416,6 +417,19 @@ class KnockViaServerTestCase(unittest.FederatingHomeserverTestCase): break time.sleep(0.1) + # The stored leave should carry the replaced knock in `unsigned`, so + # that clients can tell a denied knock from a plain kick. + assert leave_event_id is not None + leave_event = self.get_success(self.store.get_event(leave_event_id)) + self.assertEqual( + leave_event.unsigned.get("prev_content", {}).get("membership"), + Membership.KNOCK, + ) + self.assertEqual( + leave_event.unsigned.get("replaces_state"), + knock_result.knock_event_id, + ) + 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."""