Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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/19431.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add stable support for [MSC4380](https://github.com/matrix-org/matrix-spec-proposals/pull/4380) invite blocking.
4 changes: 1 addition & 3 deletions synapse/api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,9 +325,7 @@ class AccountDataTypes:
"org.matrix.msc4155.invite_permission_config"
)
# MSC4380: Invite blocking
MSC4380_INVITE_PERMISSION_CONFIG: Final = (
"org.matrix.msc4380.invite_permission_config"
)
INVITE_PERMISSION_CONFIG: Final = "m.invite_permission_config"
# Synapse-specific behaviour. See "Client-Server API Extensions" documentation
# in Admin API for more information.
SYNAPSE_ADMIN_CLIENT_CONFIG: Final = "io.element.synapse.admin_client_config"
Expand Down
2 changes: 1 addition & 1 deletion synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class Codes(str, Enum):
KEY_TOO_LARGE = "M_KEY_TOO_LARGE"

# Part of MSC4155/MSC4380
INVITE_BLOCKED = "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED"
INVITE_BLOCKED = "M_INVITE_BLOCKED"

# Part of MSC4190
APPSERVICE_LOGIN_UNSUPPORTED = "IO.ELEMENT.MSC4190.M_APPSERVICE_LOGIN_UNSUPPORTED"
Expand Down
3 changes: 0 additions & 3 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,3 @@ def read_config(
# Note that sticky events persisted before this feature is enabled will not be
# considered sticky by the local homeserver.
self.msc4354_enabled: bool = experimental.get("msc4354_enabled", False)

# MSC4380: Invite blocking
self.msc4380_enabled: bool = experimental.get("msc4380_enabled", False)
2 changes: 1 addition & 1 deletion synapse/rest/client/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ async def on_GET(self, request: SynapseRequest) -> tuple[int, JsonDict]:
# MSC4354: Sticky events
"org.matrix.msc4354": self.config.experimental.msc4354_enabled,
# MSC4380: Invite blocking
"org.matrix.msc4380": self.config.experimental.msc4380_enabled,
"org.matrix.msc4380.stable": True,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my own reference:

Once this proposal completes FCP, servers may advertise support for the stable identifiers by listing org.matrix.msc4380.stable in unstable_features; clients may use this while they are waiting for the server to adopt a version of the spec that includes it.

},
},
)
Expand Down
16 changes: 7 additions & 9 deletions synapse/storage/databases/main/account_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ def __init__(
)

self._msc4155_enabled = hs.config.experimental.msc4155_enabled
self._msc4380_enabled = hs.config.experimental.msc4380_enabled

def get_max_account_data_stream_id(self) -> int:
"""Get the current max stream ID for account data stream
Expand Down Expand Up @@ -573,14 +572,13 @@ async def get_invite_config_for_user(self, user_id: str) -> InviteRulesConfig:
Args:
user_id: The user whose invite configuration should be returned.
"""
if self._msc4380_enabled:
data = await self.get_global_account_data_by_type_for_user(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like it wouldn't cost much to also read the unstable invite rule here, what's the reasoning for not keeping that? Is this because we'd prefer running a one-time migration?

Copy link
Copy Markdown
Member Author

@richvdh richvdh Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I'm not really expecting anyone to have a org.matrix.msc4380.invite_permission_config account data. For anyone to have set it and it to have had an effect, they must have deployed their own server and enabled the experimental option, so they should have known that they were using an experimental feature that they should have expected to break under them. So reading the unstable account data would be code that we have to carry around (for ever?) for no real purpose.

  2. I'm pretty opposed to Synapse implementing unstable features out of the box, because it's a slippery slope where people come to rely on the unstable feature existing and we end up having to spec it despite it having a stupid name.

    So, if we were to keep this, we'd have to keep the corresponding experimental config flag to opt into it, which means there is even more code that we have to lug around. (And, IMHO, there are already far too many experimental flags; we ought to be looking to rip them out, not allow then to proliferate.)

In short: I'm 👎 on keeping support for the unstable account data.

user_id, AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG
)
# If the user has an MSC4380-style config setting, prioritise that
# above an MSC4155 one
if data is not None:
return MSC4380InviteRulesConfig.from_account_data(data)
data = await self.get_global_account_data_by_type_for_user(
user_id, AccountDataTypes.INVITE_PERMISSION_CONFIG
)
# If the user has an MSC4380-style config setting, prioritise that
# above an MSC4155 one
if data is not None:
return MSC4380InviteRulesConfig.from_account_data(data)

if self._msc4155_enabled:
data = await self.get_global_account_data_by_type_for_user(
Expand Down
43 changes: 8 additions & 35 deletions tests/handlers/test_room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ def test_misc4155_block_invite_local(self) -> None:
SynapseError,
).value
self.assertEqual(f.code, 403)
self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED")
self.assertEqual(f.errcode, "M_INVITE_BLOCKED")

@override_config({"experimental_features": {"msc4155_enabled": False}})
def test_msc4155_disabled_allow_invite_local(self) -> None:
Expand Down Expand Up @@ -573,7 +573,7 @@ def test_msc4155_block_invite_remote(self) -> None:
SynapseError,
).value
self.assertEqual(f.code, 403)
self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED")
self.assertEqual(f.errcode, "M_INVITE_BLOCKED")

@override_config({"experimental_features": {"msc4155_enabled": True}})
def test_msc4155_block_invite_remote_server(self) -> None:
Expand Down Expand Up @@ -619,7 +619,7 @@ def test_msc4155_block_invite_remote_server(self) -> None:
SynapseError,
).value
self.assertEqual(f.code, 403)
self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED")
self.assertEqual(f.errcode, "M_INVITE_BLOCKED")


class TestMSC4380InviteBlocking(FederatingHomeserverTestCase):
Expand All @@ -642,15 +642,14 @@ def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.bob = self.register_user("bob", "pass")
self.bob_token = self.login("bob", "pass")

@override_config({"experimental_features": {"msc4380_enabled": True}})
def test_misc4380_block_invite_local(self) -> None:
"""Test that MSC4380 will block a user from being invited to a room"""
room_id = self.helper.create_room_as(self.alice, tok=self.alice_token)

self.get_success(
self.store.add_account_data_for_user(
self.bob,
AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG,
AccountDataTypes.INVITE_PERMISSION_CONFIG,
{
"default_action": "block",
},
Expand All @@ -667,17 +666,16 @@ def test_misc4380_block_invite_local(self) -> None:
SynapseError,
).value
self.assertEqual(f.code, 403)
self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED")
self.assertEqual(f.errcode, "M_INVITE_BLOCKED")

@override_config({"experimental_features": {"msc4380_enabled": True}})
def test_misc4380_non_string_setting(self) -> None:
"""Test that `default_action` being set to something non-stringy is the same as "accept"."""
room_id = self.helper.create_room_as(self.alice, tok=self.alice_token)

self.get_success(
self.store.add_account_data_for_user(
self.bob,
AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG,
AccountDataTypes.INVITE_PERMISSION_CONFIG,
{
"default_action": 1,
},
Expand All @@ -693,31 +691,6 @@ def test_misc4380_non_string_setting(self) -> None:
)
)

@override_config({"experimental_features": {"msc4380_enabled": False}})
def test_msc4380_disabled_allow_invite_local(self) -> None:
"""Test that, when MSC4380 is not enabled, invites are accepted as normal"""
room_id = self.helper.create_room_as(self.alice, tok=self.alice_token)

self.get_success(
self.store.add_account_data_for_user(
self.bob,
AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG,
{
"default_action": "block",
},
)
)

self.get_success(
self.handler.update_membership(
requester=create_requester(self.alice),
target=UserID.from_string(self.bob),
room_id=room_id,
action=Membership.INVITE,
),
)

@override_config({"experimental_features": {"msc4380_enabled": True}})
def test_msc4380_block_invite_remote(self) -> None:
"""Test that MSC4380 will block a user from being invited to a room by a remote user."""
# A remote user who sends the invite
Expand All @@ -727,7 +700,7 @@ def test_msc4380_block_invite_remote(self) -> None:
self.get_success(
self.store.add_account_data_for_user(
self.bob,
AccountDataTypes.MSC4380_INVITE_PERMISSION_CONFIG,
AccountDataTypes.INVITE_PERMISSION_CONFIG,
{"default_action": "block"},
)
)
Expand Down Expand Up @@ -761,4 +734,4 @@ def test_msc4380_block_invite_remote(self) -> None:
SynapseError,
).value
self.assertEqual(f.code, 403)
self.assertEqual(f.errcode, "ORG.MATRIX.MSC4155.M_INVITE_BLOCKED")
self.assertEqual(f.errcode, "M_INVITE_BLOCKED")
Loading