Merge #20450: Allow loading the synthetic join of a Server Announcement room with /context

This commit is contained in:
Quentin Gliech committed 2026-11-03 09:26:55 +01:00
commit 0e0fc9e4e7
4 files changed
+137 -12

No files matched your search

+33 -1
View File
@@ -75,6 +75,10 @@ from synapse.events.utils import (
copy_and_fixup_power_levels_contents,
)
from synapse.handlers.relations import BundledAggregations
from synapse.handlers.server_announcements import (
make_synthetic_join_event,
make_synthetic_join_event_id,
)
from synapse.rest.admin._base import assert_user_is_admin
from synapse.streams import EventSource
from synapse.types import (
@@ -122,6 +126,7 @@ class EventContext:
aggregations: dict[str, BundledAggregations]
start: str
end: str
synthetic_event: JsonDict | None = None
class RoomCreationHandler:
@@ -2023,6 +2028,10 @@ class RoomContextHandler:
"""Retrieves events, pagination tokens and state around a given event
in a room.
The requester's synthetic join in an announcement room is never persisted.
It is returned as the `synthetic_event` of the result, at the position of
the room's create event, which is not itself returned to the requester.
Args:
requester
room_id
@@ -2060,13 +2069,35 @@ class RoomContextHandler:
is_peeking=is_peeking,
)
# When requesting the context of a synthetic announcement room membership
# the context displayed will be based on the create event
synthetic_join = None
if (
not use_admin_priviledge
and event_id == make_synthetic_join_event_id(room_id, user.to_string())
and await self.store.get_server_announcement_id(room_id) is not None
):
create_event = await self.store.get_create_event_for_room(room_id)
synthetic_join = make_synthetic_join_event(
room_id, user.to_string(), create_event.origin_server_ts
)
event_id = create_event.event_id
event = await self.store.get_event(
event_id, get_prev_content=True, allow_none=True
)
if not event:
return None
filtered = await filter_evts([event])
if synthetic_join is not None:
# Here the `event` is the create event of the room. A user peeking at the
# room cannot pass filter_evts() on it, since the history_visibility is set
# after the create event. This is fine as the event is not returned to the
# requester.
filtered = [FilteredEvent.state(event)]
else:
filtered = await filter_evts([event])
if not filtered:
raise AuthError(403, "You don't have permission to access that event.")
@@ -2140,6 +2171,7 @@ class RoomContextHandler:
end=await token.copy_and_replace(StreamKeyType.ROOM, results.end).to_string(
self.store
),
synthetic_event=synthetic_join,
)
+7 -2
View File
@@ -43,9 +43,14 @@ anything in yet is not listed.
def make_synthetic_join_event_id(room_id: str, user_id: str) -> str:
"""Return the event ID of the synthetic join of `user_id` in `room_id`.
"""Build the event ID of the synthetic join of a user in an announcement room.
See `make_synthetic_join_event`.
Args:
room_id: The announcement room.
user_id: The user receiving the event.
Returns:
The event ID, which is the same every time for a given room and user.
"""
return "-".join(
[
+14 -6
View File
@@ -1280,6 +1280,19 @@ class RoomEventContextServlet(RestServlet):
serializer_options = await self._event_serializer.create_config(
requester=requester
)
if event_context.synthetic_event is not None:
event = self._event_serializer.serialize_synthetic_event(
event_context.synthetic_event, serializer_options
)
else:
event = await self._event_serializer.serialize_event(
event_context.event,
time_now,
bundle_aggregations=event_context.aggregations,
config=serializer_options,
)
results = {
"events_before": await self._event_serializer.serialize_events(
event_context.events_before,
@@ -1287,12 +1300,7 @@ class RoomEventContextServlet(RestServlet):
bundle_aggregations=event_context.aggregations,
config=serializer_options,
),
"event": await self._event_serializer.serialize_event(
event_context.event,
time_now,
bundle_aggregations=event_context.aggregations,
config=serializer_options,
),
"event": event,
"events_after": await self._event_serializer.serialize_events(
event_context.events_after,
time_now,
+83 -3
View File
@@ -18,7 +18,8 @@ from twisted.internet.testing import MemoryReactor
import synapse.rest.admin
from synapse.api.constants import EventTypes, RelationTypes
from synapse.config.server_announcements import ServerAnnouncementRoom
from synapse.rest.client import login, relations, room
from synapse.handlers.server_announcements import make_synthetic_join_event_id
from synapse.rest.client import login, relations, room, sync
from synapse.server import HomeServer
from synapse.types import JsonDict
from synapse.util.clock import Clock
@@ -52,12 +53,13 @@ class ServerAnnouncementsHistoryTestCase(unittest.HomeserverTestCase):
room.register_servlets,
room.register_deprecated_servlets,
relations.register_servlets,
sync.register_servlets,
]
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.announcer = self.register_user("announcer", "pass")
self.announcer_tok = self.login("announcer", "pass")
reader = self.register_user("reader", "pass")
self.reader = self.register_user("reader", "pass")
self.reader_tok = self.login("reader", "pass")
room_config = ServerAnnouncementRoom(
@@ -82,7 +84,7 @@ class ServerAnnouncementsHistoryTestCase(unittest.HomeserverTestCase):
room_topic=room_config.room_topic,
room_avatar_url=room_config.room_avatar_url,
room_avatar_path=room_config.room_avatar_path,
allowed_user_ids=[self.announcer, reader],
allowed_user_ids=[self.announcer, self.reader],
)
]
self.get_success(handler.reconcile_rooms())
@@ -135,6 +137,84 @@ class ServerAnnouncementsHistoryTestCase(unittest.HomeserverTestCase):
body = self._get(f"/rooms/{self.room_id}/context/{self.announcement_id}")
self.assertEqual(body["event"]["event_id"], self.announcement_id)
def _get_synthetic_join(self) -> JsonDict:
"""Get the reader's synthetic join from `/sync`."""
room = self._get("/sync")["rooms"]["join"][self.room_id]
return next(
event
for event in room["state"]["events"]
if event["type"] == EventTypes.Member and event["state_key"] == self.reader
)
def test_context_of_synthetic_join(self) -> None:
"""The reader can load the synthetic join they received in `/sync`."""
join = self._get_synthetic_join()
body = self._get(f"/rooms/{self.room_id}/context/{join['event_id']}")
self.assertEqual(
body["event"], {**join, "room_id": self.room_id, "user_id": self.reader}
)
def test_context_of_synthetic_join_forward_pagination(self) -> None:
"""The reader can paginate forwards from the synthetic join to the
announcement."""
join = self._get_synthetic_join()
context = self._get(f"/rooms/{self.room_id}/context/{join['event_id']}")
body = self._get(
f"/rooms/{self.room_id}/messages?dir=f&limit=100&from={context['end']}"
)
self.assertIn(
self.announcement_id, [event["event_id"] for event in body["chunk"]]
)
def test_context_of_synthetic_join_surrounding_events(self) -> None:
"""The synthetic join comes first in the room, followed by the events the
reader can see."""
join = self._get_synthetic_join()
body = self._get(f"/rooms/{self.room_id}/context/{join['event_id']}?limit=100")
self.assertEqual(body["events_before"], [])
self.assertIn(
self.announcement_id, [event["event_id"] for event in body["events_after"]]
)
def test_context_of_synthetic_join_of_another_user(self) -> None:
"""The reader cannot load the synthetic join of another user."""
event_id = make_synthetic_join_event_id(self.room_id, self.announcer)
channel = self.make_request(
"GET",
f"/rooms/{self.room_id}/context/{event_id}",
access_token=self.reader_tok,
)
self.assertEqual(channel.code, 404, channel.json_body)
def test_context_of_synthetic_join_after_promotion(self) -> None:
"""The reader can still load the synthetic join they received in `/sync`
after a configuration change makes them a sender, and so a member."""
join = self._get_synthetic_join()
config = self.hs.config.serverannouncements
room_config = config.rooms[0]
config.rooms = [
ServerAnnouncementRoom(
config_id=room_config.config_id,
room_name=room_config.room_name,
room_topic=room_config.room_topic,
room_avatar_url=room_config.room_avatar_url,
room_avatar_path=room_config.room_avatar_path,
allowed_user_ids=[self.announcer, self.reader],
)
]
self.get_success(self.hs.get_server_announcements_handler().reconcile_rooms())
self.assertIn(self.room_id, self._get("/joined_rooms")["joined_rooms"])
body = self._get(f"/rooms/{self.room_id}/context/{join['event_id']}")
self.assertEqual(
body["event"], {**join, "room_id": self.room_id, "user_id": self.reader}
)
def test_event(self) -> None:
"""The reader can fetch the announcement by its ID."""
body = self._get(f"/rooms/{self.room_id}/event/{self.announcement_id}")