Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c6e994f
Fix structure of initial_state when calling user_may_create_room modu…
hughns Jul 23, 2025
9f2539c
Changelog
hughns Jul 23, 2025
99f5de0
Apply suggestions from code review
hughns Jul 25, 2025
831773d
More suggested changes from review
hughns Jul 25, 2025
137da57
More suggestions from code review
hughns Jul 25, 2025
fa42783
More suggestions from review
hughns Jul 25, 2025
9203016
Pass `room_config` = `None` for a room upgrade and clarify documentation
hughns Jul 28, 2025
005a61f
Suggestions from review
hughns Jul 28, 2025
2e115b7
Revert the logic back to how it was pre adding room_config
hughns Jul 28, 2025
9c13a60
Revert "Revert the logic back to how it was pre adding room_config"
hughns Aug 29, 2025
3d17a88
Revert "Suggestions from review"
hughns Aug 29, 2025
44a0e76
Revert "Pass `room_config` = `None` for a room upgrade and clarify do…
hughns Aug 29, 2025
8693873
Update documentation
hughns Aug 29, 2025
ce47547
Merge branch 'develop' into hughns/user-may-create-room-initial-state…
hughns Aug 29, 2025
ca9d34c
Merge branch 'develop' into hughns/user-may-create-room-initial-state…
hughns Sep 12, 2025
a165f99
Another attempt at line spacing in tests
hughns Sep 17, 2025
dcc5ce3
Update docs/modules/spam_checker_callbacks.md
hughns Sep 17, 2025
91becca
Linewrap
hughns Sep 17, 2025
202aeca
Merge branch 'develop' into hughns/user-may-create-room-initial-state…
hughns Sep 17, 2025
14895d3
Apply suggestions from code review
hughns Sep 24, 2025
1f9b3e3
Improve comments for spam checker logic
hughns Sep 24, 2025
2bb2da4
Merge branch 'develop' into hughns/user-may-create-room-initial-state…
hughns Sep 24, 2025
4c6942c
Add TODO comments regarding preset for spam checker.
hughns Sep 24, 2025
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/18721.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix room upgrade `room_config` argument and documentation for `user_may_create_room` spam-checker callback.
7 changes: 5 additions & 2 deletions docs/modules/spam_checker_callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,15 @@ _Changed in Synapse v1.132.0: Added the `room_config` argument. Callbacks that o
async def user_may_create_room(user_id: str, room_config: synapse.module_api.JsonDict) -> Union["synapse.module_api.NOT_SPAM", "synapse.module_api.errors.Codes", bool]
```

Called when processing a room creation request.
Called when processing a room creation or room upgrade request.

The arguments passed to this callback are:

* `user_id`: The Matrix user ID of the user (e.g. `@alice:example.com`).
* `room_config`: The contents of the body of a [/createRoom request](https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3createroom) as a dictionary.
* `room_config`: The contents of the body of the [`/createRoom` request](https://spec.matrix.org/v1.15/client-server-api/#post_matrixclientv3createroom) as a dictionary.
For a [room upgrade request](https://spec.matrix.org/v1.15/client-server-api/#post_matrixclientv3roomsroomidupgrade) it is a synthesised subset of what an equivalent
`/createRoom` request would have looked like. Specifically, it contains the `creation_content` (linking to the previous room) and `initial_state` (containing a
subset of the state of the previous room).

The callback must return one of:
- `synapse.module_api.NOT_SPAM`, to allow the operation. Other callbacks may still
Expand Down
17 changes: 13 additions & 4 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,7 +597,7 @@ async def clone_existing_room(
new_room_version,
additional_creators=additional_creators,
)
initial_state = {}
initial_state: MutableStateMap = {}

# Replicate relevant room events
types_to_copy: List[Tuple[str, Optional[str]]] = [
Expand Down Expand Up @@ -693,14 +693,23 @@ async def clone_existing_room(
additional_creators,
)

# We construct what the body of a call to /createRoom would look like for passing
# to the spam checker. We don't include a preset here, as we expect the
# We construct a subset of what the body of a call to /createRoom would look like
# for passing to the spam checker. We don't include a preset here, as we expect the
# initial state to contain everything we need.
# TODO: given we are upgrading, it would make sense to pass the room_version
# TODO: the preset might be useful too
spam_check = await self._spam_checker_module_callbacks.user_may_create_room(
user_id,
{
"creation_content": creation_content,
"initial_state": list(initial_state.items()),
"initial_state": [
{
"type": state_key[0],
"state_key": state_key[1],
"content": event_content,
}
for state_key, event_content in initial_state.items()
],
},
)
if spam_check != self._spam_checker_module_callbacks.NOT_SPAM:
Expand Down
70 changes: 56 additions & 14 deletions tests/module_api/test_spamchecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from twisted.internet.testing import MemoryReactor

from synapse.api.constants import EventContentFields, EventTypes
from synapse.config.server import DEFAULT_ROOM_VERSION
from synapse.rest import admin, login, room, room_upgrade_rest_servlet
from synapse.server import HomeServer
Expand Down Expand Up @@ -51,8 +52,8 @@ def create_room(self, content: JsonDict) -> FakeChannel:

return channel

def test_may_user_create_room(self) -> None:
"""Test that the may_user_create_room callback is called when a user
def test_user_may_create_room(self) -> None:
"""Test that the user_may_create_room callback is called when a user
creates a room, and that it receives the correct parameters.
"""

Expand All @@ -67,16 +68,50 @@ async def user_may_create_room(
user_may_create_room=user_may_create_room
)

channel = self.create_room({"foo": "baa"})
expected_room_config = {"foo": "baa"}
channel = self.create_room(expected_room_config)

self.assertEqual(channel.code, 200)
self.assertEqual(self.last_user_id, self.user_id)
self.assertEqual(self.last_room_config["foo"], "baa")
self.assertEqual(self.last_room_config, expected_room_config)

def test_may_user_create_room_on_upgrade(self) -> None:
"""Test that the may_user_create_room callback is called when a room is upgraded."""
def test_user_may_create_room_with_initial_state(self) -> None:
"""Test that the user_may_create_room callback is called when a user
creates a room with some initial state events, and that it receives the correct parameters.
"""

async def user_may_create_room(
user_id: str, room_config: JsonDict
) -> Union[Literal["NOT_SPAM"], Codes]:
self.last_room_config = room_config
self.last_user_id = user_id
return "NOT_SPAM"

self._module_api.register_spam_checker_callbacks(
user_may_create_room=user_may_create_room
)

expected_room_config = {
"foo": "baa",
"initial_state": [
{
"type": EventTypes.Topic,
"content": {EventContentFields.TOPIC: "foo"},
}
],
}
channel = self.create_room(expected_room_config)

self.assertEqual(channel.code, 200)
self.assertEqual(self.last_user_id, self.user_id)
self.assertEqual(self.last_room_config, expected_room_config)

def test_user_may_create_room_on_upgrade(self) -> None:
"""Test that the user_may_create_room callback is called when a room is upgraded."""

# First, create a room to upgrade.
channel = self.create_room({"topic": "foo"})
channel = self.create_room({EventContentFields.TOPIC: "foo"})

self.assertEqual(channel.code, 200)
room_id = channel.json_body["room_id"]

Expand Down Expand Up @@ -107,13 +142,15 @@ async def user_may_create_room(
# Check that the initial state received by callback contains the topic event.
self.assertTrue(
any(
event[0][0] == "m.room.topic" and event[1].get("topic") == "foo"
event.get("type") == EventTypes.Topic
and event.get("state_key") == ""
and event.get("content").get(EventContentFields.TOPIC) == "foo"
for event in self.last_room_config["initial_state"]
)
)

def test_may_user_create_room_disallowed(self) -> None:
"""Test that the codes response from may_user_create_room callback is respected
def test_user_may_create_room_disallowed(self) -> None:
"""Test that the codes response from user_may_create_room callback is respected
and returned via the API.
"""

Expand All @@ -128,14 +165,16 @@ async def user_may_create_room(
user_may_create_room=user_may_create_room
)

channel = self.create_room({"foo": "baa"})
expected_room_config = {"foo": "baa"}
channel = self.create_room(expected_room_config)

self.assertEqual(channel.code, 403)
self.assertEqual(channel.json_body["errcode"], Codes.UNAUTHORIZED)
self.assertEqual(self.last_user_id, self.user_id)
self.assertEqual(self.last_room_config["foo"], "baa")
self.assertEqual(self.last_room_config, expected_room_config)

def test_may_user_create_room_compatibility(self) -> None:
"""Test that the may_user_create_room callback is called when a user
def test_user_may_create_room_compatibility(self) -> None:
"""Test that the user_may_create_room callback is called when a user
creates a room for a module that uses the old callback signature
(without the `room_config` parameter)
"""
Expand All @@ -151,6 +190,7 @@ async def user_may_create_room(
)

channel = self.create_room({"foo": "baa"})

self.assertEqual(channel.code, 200)
self.assertEqual(self.last_user_id, self.user_id)

Expand Down Expand Up @@ -178,6 +218,7 @@ async def user_may_send_state_event(
)

channel = self.create_room({})

self.assertEqual(channel.code, 200)

room_id = channel.json_body["room_id"]
Expand Down Expand Up @@ -222,6 +263,7 @@ async def user_may_send_state_event(
)

channel = self.create_room({})

self.assertEqual(channel.code, 200)

room_id = channel.json_body["room_id"]
Expand Down
Loading