Skip to content

Commit e5d3bfb

Browse files
Sliding Sync: Include invite, ban, kick, targets when $LAZY-loading room members (#17947)
Part of element-hq/synapse#17929
1 parent 9b2ae62 commit e5d3bfb

File tree

5 files changed

+181
-11
lines changed

5 files changed

+181
-11
lines changed

changelog.d/17947.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Update [MSC4186](https://github.com/matrix-org/matrix-spec-proposals/pull/4186) Sliding Sync to include invite, ban, kick, targets when `$LAZY`-loading room members.

synapse/api/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ class EventContentFields:
231231
ROOM_NAME: Final = "name"
232232

233233
MEMBERSHIP: Final = "membership"
234+
MEMBERSHIP_DISPLAYNAME: Final = "displayname"
235+
MEMBERSHIP_AVATAR_URL: Final = "avatar_url"
234236

235237
# Used in m.room.guest_access events.
236238
GUEST_ACCESS: Final = "guest_access"

synapse/handlers/sliding_sync/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -955,15 +955,21 @@ async def get_room_sync_data(
955955
and state_key == StateValues.LAZY
956956
):
957957
lazy_load_room_members = True
958+
958959
# Everyone in the timeline is relevant
959-
#
960-
# FIXME: We probably also care about invite, ban, kick, targets, etc
961-
# but the spec only mentions "senders".
962960
timeline_membership: Set[str] = set()
963961
if timeline_events is not None:
964962
for timeline_event in timeline_events:
963+
# Anyone who sent a message is relevant
965964
timeline_membership.add(timeline_event.sender)
966965

966+
# We also care about invite, ban, kick, targets,
967+
# etc.
968+
if timeline_event.type == EventTypes.Member:
969+
timeline_membership.add(
970+
timeline_event.state_key
971+
)
972+
967973
# Update the required state filter so we pick up the new
968974
# membership
969975
for user_id in timeline_membership:

synapse/types/handlers/sliding_sync.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -407,8 +407,8 @@ class StateValues:
407407
# Include all state events of the given type
408408
WILDCARD: Final = "*"
409409
# Lazy-load room membership events (include room membership events for any event
410-
# `sender` in the timeline). We only give special meaning to this value when it's a
411-
# `state_key`.
410+
# `sender` or membership change target in the timeline). We only give special
411+
# meaning to this value when it's a `state_key`.
412412
LAZY: Final = "$LAZY"
413413
# Subsitute with the requester's user ID. Typically used by clients to get
414414
# the user's membership.
@@ -641,9 +641,10 @@ def must_await_full_state(
641641
if user_id == StateValues.ME:
642642
continue
643643
# We're lazy-loading membership so we can just return the state we have.
644-
# Lazy-loading means we include membership for any event `sender` in the
645-
# timeline but since we had to auth those timeline events, we will have the
646-
# membership state for them (including from remote senders).
644+
# Lazy-loading means we include membership for any event `sender` or
645+
# membership change target in the timeline but since we had to auth those
646+
# timeline events, we will have the membership state for them (including
647+
# from remote senders).
647648
elif user_id == StateValues.LAZY:
648649
continue
649650
elif user_id == StateValues.WILDCARD:

tests/rest/client/sliding_sync/test_rooms_required_state.py

Lines changed: 163 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@
1111
# See the GNU Affero General Public License for more details:
1212
# <https://www.gnu.org/licenses/agpl-3.0.html>.
1313
#
14+
import enum
1415
import logging
1516

1617
from parameterized import parameterized, parameterized_class
1718

1819
from twisted.test.proto_helpers import MemoryReactor
1920

2021
import synapse.rest.admin
21-
from synapse.api.constants import EventTypes, Membership
22+
from synapse.api.constants import EventContentFields, EventTypes, JoinRules, Membership
2223
from synapse.handlers.sliding_sync import StateValues
23-
from synapse.rest.client import login, room, sync
24+
from synapse.rest.client import knock, login, room, sync
2425
from synapse.server import HomeServer
2526
from synapse.util import Clock
2627

@@ -30,6 +31,17 @@
3031
logger = logging.getLogger(__name__)
3132

3233

34+
# Inherit from `str` so that they show up in the test description when we
35+
# `@parameterized.expand(...)` the first parameter
36+
class MembershipAction(str, enum.Enum):
37+
INVITE = "invite"
38+
JOIN = "join"
39+
KNOCK = "knock"
40+
LEAVE = "leave"
41+
BAN = "ban"
42+
KICK = "kick"
43+
44+
3345
# FIXME: This can be removed once we bump `SCHEMA_COMPAT_VERSION` and run the
3446
# foreground update for
3547
# `sliding_sync_joined_rooms`/`sliding_sync_membership_snapshots` (tracked by
@@ -52,6 +64,7 @@ class SlidingSyncRoomsRequiredStateTestCase(SlidingSyncBase):
5264
servlets = [
5365
synapse.rest.admin.register_servlets,
5466
login.register_servlets,
67+
knock.register_servlets,
5568
room.register_servlets,
5669
sync.register_servlets,
5770
]
@@ -496,6 +509,153 @@ def test_rooms_required_state_lazy_loading_room_members_incremental_sync(
496509
)
497510
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
498511

512+
@parameterized.expand(
513+
[
514+
(MembershipAction.LEAVE,),
515+
(MembershipAction.INVITE,),
516+
(MembershipAction.KNOCK,),
517+
(MembershipAction.JOIN,),
518+
(MembershipAction.BAN,),
519+
(MembershipAction.KICK,),
520+
]
521+
)
522+
def test_rooms_required_state_changed_membership_in_timeline_lazy_loading_room_members_incremental_sync(
523+
self,
524+
room_membership_action: str,
525+
) -> None:
526+
"""
527+
On incremental sync, test `rooms.required_state` returns people relevant to the
528+
timeline when lazy-loading room members, `["m.room.member","$LAZY"]` **including
529+
changes to membership**.
530+
"""
531+
user1_id = self.register_user("user1", "pass")
532+
user1_tok = self.login(user1_id, "pass")
533+
user2_id = self.register_user("user2", "pass")
534+
user2_tok = self.login(user2_id, "pass")
535+
user3_id = self.register_user("user3", "pass")
536+
user3_tok = self.login(user3_id, "pass")
537+
user4_id = self.register_user("user4", "pass")
538+
user4_tok = self.login(user4_id, "pass")
539+
user5_id = self.register_user("user5", "pass")
540+
user5_tok = self.login(user5_id, "pass")
541+
542+
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok, is_public=True)
543+
# If we're testing knocks, set the room to knock
544+
if room_membership_action == MembershipAction.KNOCK:
545+
self.helper.send_state(
546+
room_id1,
547+
EventTypes.JoinRules,
548+
{"join_rule": JoinRules.KNOCK},
549+
tok=user2_tok,
550+
)
551+
552+
# Join the test users to the room
553+
self.helper.invite(room_id1, src=user2_id, targ=user1_id, tok=user2_tok)
554+
self.helper.join(room_id1, user1_id, tok=user1_tok)
555+
self.helper.invite(room_id1, src=user2_id, targ=user3_id, tok=user2_tok)
556+
self.helper.join(room_id1, user3_id, tok=user3_tok)
557+
self.helper.invite(room_id1, src=user2_id, targ=user4_id, tok=user2_tok)
558+
self.helper.join(room_id1, user4_id, tok=user4_tok)
559+
if room_membership_action in (
560+
MembershipAction.LEAVE,
561+
MembershipAction.BAN,
562+
MembershipAction.JOIN,
563+
):
564+
self.helper.invite(room_id1, src=user2_id, targ=user5_id, tok=user2_tok)
565+
self.helper.join(room_id1, user5_id, tok=user5_tok)
566+
567+
# Send some messages to fill up the space
568+
self.helper.send(room_id1, "1", tok=user2_tok)
569+
self.helper.send(room_id1, "2", tok=user2_tok)
570+
self.helper.send(room_id1, "3", tok=user2_tok)
571+
572+
# Make the Sliding Sync request with lazy loading for the room members
573+
sync_body = {
574+
"lists": {
575+
"foo-list": {
576+
"ranges": [[0, 1]],
577+
"required_state": [
578+
[EventTypes.Create, ""],
579+
[EventTypes.Member, StateValues.LAZY],
580+
],
581+
"timeline_limit": 3,
582+
}
583+
}
584+
}
585+
response_body, from_token = self.do_sync(sync_body, tok=user1_tok)
586+
587+
# Send more timeline events into the room
588+
self.helper.send(room_id1, "4", tok=user2_tok)
589+
self.helper.send(room_id1, "5", tok=user4_tok)
590+
# The third event will be our membership event concerning user5
591+
if room_membership_action == MembershipAction.LEAVE:
592+
# User 5 leaves
593+
self.helper.leave(room_id1, user5_id, tok=user5_tok)
594+
elif room_membership_action == MembershipAction.INVITE:
595+
# User 5 is invited
596+
self.helper.invite(room_id1, src=user2_id, targ=user5_id, tok=user2_tok)
597+
elif room_membership_action == MembershipAction.KNOCK:
598+
# User 5 knocks
599+
self.helper.knock(room_id1, user5_id, tok=user5_tok)
600+
# The admin of the room accepts the knock
601+
self.helper.invite(room_id1, src=user2_id, targ=user5_id, tok=user2_tok)
602+
elif room_membership_action == MembershipAction.JOIN:
603+
# Update the display name of user5 (causing a membership change)
604+
self.helper.send_state(
605+
room_id1,
606+
event_type=EventTypes.Member,
607+
state_key=user5_id,
608+
body={
609+
EventContentFields.MEMBERSHIP: Membership.JOIN,
610+
EventContentFields.MEMBERSHIP_DISPLAYNAME: "quick changer",
611+
},
612+
tok=user5_tok,
613+
)
614+
elif room_membership_action == MembershipAction.BAN:
615+
self.helper.ban(room_id1, src=user2_id, targ=user5_id, tok=user2_tok)
616+
elif room_membership_action == MembershipAction.KICK:
617+
# Kick user5 from the room
618+
self.helper.change_membership(
619+
room=room_id1,
620+
src=user2_id,
621+
targ=user5_id,
622+
tok=user2_tok,
623+
membership=Membership.LEAVE,
624+
extra_data={
625+
"reason": "Bad manners",
626+
},
627+
)
628+
else:
629+
raise AssertionError(
630+
f"Unknown room_membership_action: {room_membership_action}"
631+
)
632+
633+
# Make an incremental Sliding Sync request
634+
response_body, _ = self.do_sync(sync_body, since=from_token, tok=user1_tok)
635+
636+
state_map = self.get_success(
637+
self.storage_controllers.state.get_current_state(room_id1)
638+
)
639+
640+
# Only user2, user4, and user5 sent events in the last 3 events we see in the
641+
# `timeline`.
642+
self._assertRequiredStateIncludes(
643+
response_body["rooms"][room_id1]["required_state"],
644+
{
645+
# This appears because *some* membership in the room changed and the
646+
# heroes are recalculated and is thrown in because we have it. But this
647+
# is technically optional and not needed because we've already seen user2
648+
# in the last sync (and their membership hasn't changed).
649+
state_map[(EventTypes.Member, user2_id)],
650+
# Appears because there is a message in the timeline from this user
651+
state_map[(EventTypes.Member, user4_id)],
652+
# Appears because there is a membership event in the timeline from this user
653+
state_map[(EventTypes.Member, user5_id)],
654+
},
655+
exact=True,
656+
)
657+
self.assertIsNone(response_body["rooms"][room_id1].get("invite_state"))
658+
499659
def test_rooms_required_state_expand_lazy_loading_room_members_incremental_sync(
500660
self,
501661
) -> None:
@@ -1243,7 +1403,7 @@ def test_rooms_required_state_expand_retract_expand(self) -> None:
12431403

12441404
# Update the room name
12451405
self.helper.send_state(
1246-
room_id1, "m.room.name", {"name": "Bar"}, state_key="", tok=user1_tok
1406+
room_id1, EventTypes.Name, {"name": "Bar"}, state_key="", tok=user1_tok
12471407
)
12481408

12491409
# Update the sliding sync requests to exclude the room name again

0 commit comments

Comments
 (0)