Add newly_joined support to get_sync_room_ids_for_user(...)

This commit is contained in:
Eric Eastwood 2024-06-26 01:10:00 -05:00
parent cccbd15e7e
commit 62c6a4e860
2 changed files with 300 additions and 6 deletions

View file

@ -18,7 +18,8 @@
#
#
import logging
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from collections import defaultdict
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
import attr
from immutabledict import immutabledict
@ -104,6 +105,9 @@ class _RoomMembershipForUser:
sender: str
newly_joined: bool
def copy_and_replace(self, **kwds: Any) -> "_RoomMembershipForUser":
return attr.evolve(self, **kwds)
class SlidingSyncHandler:
def __init__(self, hs: "HomeServer"):
@ -414,6 +418,7 @@ class SlidingSyncHandler:
# - 1b) Add back rooms that the user left after the `to_token`
# - 1c) Update room membership events to the point in time of the `to_token`
# - 2) Add back newly_left rooms (> `from_token` and <= `to_token`)
# - 3) Figure out which rooms are `newly_joined`
# 1) -----------------------------------------------------
@ -529,19 +534,49 @@ class SlidingSyncHandler:
last_membership_change_by_room_id_in_from_to_range: Dict[
str, CurrentStateDeltaMembership
] = {}
# We also want to assemble a list of the first membership events during the token
# range so we can step backward to the previous membership that would apply to
# before the token range to see if we have `newly_joined` the room.
first_membership_change_by_room_id_in_from_to_range: Dict[
str, CurrentStateDeltaMembership
] = {}
non_join_event_ids_by_room_id_in_from_to_range: Dict[str, List[str]] = (
defaultdict(list)
)
for (
membership_change
) in current_state_delta_membership_changes_in_from_to_range:
last_membership_change_by_room_id_in_from_to_range[
membership_change.room_id
] = membership_change
room_id = membership_change.room_id
last_membership_change_by_room_id_in_from_to_range[room_id] = (
membership_change
)
# Only set if we haven't already set it
first_membership_change_by_room_id_in_from_to_range.setdefault(
room_id, membership_change
)
if membership_change.membership != Membership.JOIN:
non_join_event_ids_by_room_id_in_from_to_range[room_id].append(
membership_change.event_id
)
# 2) Fixup
#
# 3) We also want to assemble a list of possibly newly joined rooms. Someone
# could have left and joined multiple times during the given range but we only
# care about whether they are joined at the end of the token range so we are
# working with the last membership even in the token range.
possibly_newly_joined_room_ids = set()
for (
last_membership_change_in_from_to_range
) in last_membership_change_by_room_id_in_from_to_range.values():
room_id = last_membership_change_in_from_to_range.room_id
if last_membership_change_in_from_to_range.membership == Membership.JOIN:
possibly_newly_joined_room_ids.add(room_id)
# 2) Add back newly_left rooms (> `from_token` and <= `to_token`). We
# include newly_left rooms because the last event that the user should see
# is their own leave event
@ -554,7 +589,44 @@ class SlidingSyncHandler:
newly_joined=False,
)
# TODO: Figure out `newly_joined`
# 3) Figure out `newly_joined`
prev_event_ids_before_token_range: List[str] = []
for possibly_newly_joined_room_id in possibly_newly_joined_room_ids:
non_joins_for_room = non_join_event_ids_by_room_id_in_from_to_range[
possibly_newly_joined_room_id
]
if len(non_joins_for_room) > 0:
# We found a `newly_joined` room (we left and joined within the token range)
filtered_sync_room_id_set[room_id] = filtered_sync_room_id_set[
room_id
].copy_and_replace(newly_joined=True)
else:
prev_event_id = first_membership_change_by_room_id_in_from_to_range[
room_id
].prev_event_id
if prev_event_id is None:
# We found a `newly_joined` room (we are joining the room for the
# first time within the token range)
filtered_sync_room_id_set[room_id] = filtered_sync_room_id_set[
room_id
].copy_and_replace(newly_joined=True)
else:
# Last resort, we need to step back to the previous membership event
# just before the token range to see if we're joined then or not.
prev_event_ids_before_token_range.append(prev_event_id)
# 3) more
prev_events_before_token_range = await self.store.get_events(
prev_event_ids_before_token_range
)
for prev_event_before_token_range in prev_events_before_token_range.values():
if prev_event_before_token_range.membership != Membership.JOIN:
# We found a `newly_joined` room (we left before the token range
# and joined within the token range)
filtered_sync_room_id_set[room_id] = filtered_sync_room_id_set[
room_id
].copy_and_replace(newly_joined=True)
return filtered_sync_room_id_set

View file

@ -116,6 +116,9 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
room_id_results[room_id].event_id,
join_response["event_id"],
)
# We should be considered `newly_joined` because we joined during the token
# range
self.assertEqual(room_id_results[room_id].newly_joined, True)
def test_get_already_joined_room(self) -> None:
"""
@ -146,6 +149,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
room_id_results[room_id].event_id,
join_response["event_id"],
)
# We should *NOT* be `newly_joined` because we joined before the token range
self.assertEqual(room_id_results[room_id].newly_joined, False)
def test_get_invited_banned_knocked_room(self) -> None:
"""
@ -232,6 +237,11 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
room_id_results[knock_room_id].event_id,
knock_room_membership_state_event.event_id,
)
# We should *NOT* be `newly_joined` because we were not joined at the the time
# of the `to_token`.
self.assertEqual(room_id_results[invited_room_id].newly_joined, False)
self.assertEqual(room_id_results[ban_room_id].newly_joined, False)
self.assertEqual(room_id_results[knock_room_id].newly_joined, False)
def test_get_kicked_room(self) -> None:
"""
@ -277,6 +287,9 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
room_id_results[kick_room_id].event_id,
kick_response["event_id"],
)
# We should *NOT* be `newly_joined` because we were not joined at the the time
# of the `to_token`.
self.assertEqual(room_id_results[kick_room_id].newly_joined, False)
def test_forgotten_rooms(self) -> None:
"""
@ -396,6 +409,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
room_id_results[room_id2].event_id,
leave_response["event_id"],
)
# We should *NOT* be `newly_joined` because we are instead `newly_left`
self.assertEqual(room_id_results[room_id2].newly_joined, False)
def test_no_joins_after_to_token(self) -> None:
"""
@ -432,6 +447,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
room_id_results[room_id1].event_id,
join_response1["event_id"],
)
# We should be `newly_joined` because we joined during the token range
self.assertEqual(room_id_results[room_id1].newly_joined, True)
def test_join_during_range_and_left_room_after_to_token(self) -> None:
"""
@ -477,6 +494,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should be `newly_joined` because we joined during the token range
self.assertEqual(room_id_results[room_id1].newly_joined, True)
def test_join_before_range_and_left_room_after_to_token(self) -> None:
"""
@ -519,6 +538,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should *NOT* be `newly_joined` because we joined before the token range
self.assertEqual(room_id_results[room_id1].newly_joined, False)
def test_kicked_before_range_and_left_after_to_token(self) -> None:
"""
@ -581,6 +602,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should *NOT* be `newly_joined` because we were kicked
self.assertEqual(room_id_results[kick_room_id].newly_joined, False)
def test_newly_left_during_range_and_join_leave_after_to_token(self) -> None:
"""
@ -632,6 +655,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should *NOT* be `newly_joined` because we left during the token range
self.assertEqual(room_id_results[room_id1].newly_joined, False)
def test_newly_left_during_range_and_join_after_to_token(self) -> None:
"""
@ -681,6 +706,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should *NOT* be `newly_joined` because we left during the token range
self.assertEqual(room_id_results[room_id1].newly_joined, False)
def test_no_from_token(self) -> None:
"""
@ -727,6 +754,9 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
room_id_results[room_id1].event_id,
join_response1["event_id"],
)
# We should *NOT* be `newly_joined` because there is no `from_token` to
# define a "live" range to compare against
self.assertEqual(room_id_results[room_id1].newly_joined, False)
def test_from_token_ahead_of_to_token(self) -> None:
"""
@ -793,6 +823,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
room_id_results[room_id1].event_id,
join_response1["event_id"],
)
# We should *NOT* be `newly_joined` because we joined `room1` before either of the tokens
self.assertEqual(room_id_results[room_id1].newly_joined, False)
def test_leave_before_range_and_join_leave_after_to_token(self) -> None:
"""
@ -920,6 +952,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should be `newly_joined` because we joined during the token range
self.assertEqual(room_id_results[room_id1].newly_joined, True)
def test_join_leave_multiple_times_before_range_and_after_to_token(
self,
@ -976,6 +1010,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should *NOT* be `newly_joined` because we joined before the token range
self.assertEqual(room_id_results[room_id1].newly_joined, False)
def test_invite_before_range_and_join_leave_after_to_token(
self,
@ -1028,8 +1064,11 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should *NOT* be `newly_joined` because we were only invited before the
# token range
self.assertEqual(room_id_results[room_id1].newly_joined, False)
def test_display_name_changes_in_token_range(
def test_join_and_display_name_changes_in_token_range(
self,
) -> None:
"""
@ -1101,6 +1140,68 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should be `newly_joined` because we joined during the token range
self.assertEqual(room_id_results[room_id1].newly_joined, True)
def test_display_name_changes_in_token_range(
self,
) -> None:
"""
Test that we point to the correct membership event within the from/to range even
if there is `displayname`/`avatar_url` updates.
"""
user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass")
user2_id = self.register_user("user2", "pass")
user2_tok = self.login(user2_id, "pass")
# We create the room with user2 so the room isn't left with no members when we
# leave and can still re-join.
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok, is_public=True)
join_response = self.helper.join(room_id1, user1_id, tok=user1_tok)
after_room1_token = self.event_sources.get_current_token()
# Update the displayname during the token range
displayname_change_during_token_range_response = self.helper.send_state(
room_id1,
event_type=EventTypes.Member,
state_key=user1_id,
body={
"membership": Membership.JOIN,
"displayname": "displayname during token range",
},
tok=user1_tok,
)
after_change1_token = self.event_sources.get_current_token()
room_id_results = self.get_success(
self.sliding_sync_handler.get_sync_room_ids_for_user(
UserID.from_string(user1_id),
from_token=after_room1_token,
to_token=after_change1_token,
)
)
# Room should show up because we were joined during the from/to range
self.assertEqual(room_id_results.keys(), {room_id1})
# It should be pointing to the latest membership event in the from/to range
self.assertEqual(
room_id_results[room_id1].event_id,
displayname_change_during_token_range_response["event_id"],
"Corresponding map to disambiguate the opaque event IDs: "
+ str(
{
"join_response": join_response["event_id"],
"displayname_change_during_token_range_response": displayname_change_during_token_range_response[
"event_id"
],
}
),
)
# We should *NOT* be `newly_joined` because we joined before the token range
self.assertEqual(room_id_results[room_id1].newly_joined, False)
def test_display_name_changes_before_and_after_token_range(
self,
@ -1172,6 +1273,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should *NOT* be `newly_joined` because we joined before the token range
self.assertEqual(room_id_results[room_id1].newly_joined, False)
def test_display_name_changes_leave_after_token_range(
self,
@ -1250,6 +1353,8 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
}
),
)
# We should be `newly_joined` because we joined during the token range
self.assertEqual(room_id_results[room_id1].newly_joined, True)
def test_display_name_changes_join_after_token_range(
self,
@ -1298,6 +1403,123 @@ class GetSyncRoomIdsForUserTestCase(HomeserverTestCase):
# Room shouldn't show up because we joined after the from/to range
self.assertEqual(room_id_results.keys(), set())
def test_newly_joined_with_leave_join_in_token_range(
self,
) -> None:
"""
Test that `newly_joined` TODO
"""
user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass")
user2_id = self.register_user("user2", "pass")
user2_tok = self.login(user2_id, "pass")
# We create the room with user2 so the room isn't left with no members when we
# leave and can still re-join.
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok, is_public=True)
self.helper.join(room_id1, user1_id, tok=user1_tok)
after_room1_token = self.event_sources.get_current_token()
# Leave and join back during the token range
self.helper.leave(room_id1, user1_id, tok=user1_tok)
join_response2 = self.helper.join(room_id1, user1_id, tok=user1_tok)
after_more_changes_token = self.event_sources.get_current_token()
room_id_results = self.get_success(
self.sliding_sync_handler.get_sync_room_ids_for_user(
UserID.from_string(user1_id),
from_token=after_room1_token,
to_token=after_more_changes_token,
)
)
# Room should show up because we were joined during the from/to range
self.assertEqual(room_id_results.keys(), {room_id1})
# It should be pointing to the latest membership event in the from/to range
self.assertEqual(
room_id_results[room_id1].event_id,
join_response2["event_id"],
)
# We should be considered `newly_joined` because there is some non-join event in
# between our latest join event.
self.assertEqual(room_id_results[room_id1].newly_joined, True)
def test_newly_joined_only_joins_during_token_range(
self,
) -> None:
"""
Test that a join and more joins caused by display name changes, all during the
token range, still count as `newly_joined`.
"""
user1_id = self.register_user("user1", "pass")
user1_tok = self.login(user1_id, "pass")
user2_id = self.register_user("user2", "pass")
user2_tok = self.login(user2_id, "pass")
before_room1_token = self.event_sources.get_current_token()
# We create the room with user2 so the room isn't left with no members when we
# leave and can still re-join.
room_id1 = self.helper.create_room_as(user2_id, tok=user2_tok, is_public=True)
# Join, leave, join back to the room before the from/to range
join_response1 = self.helper.join(room_id1, user1_id, tok=user1_tok)
# Update the displayname during the token range (looks like another join)
displayname_change_during_token_range_response1 = self.helper.send_state(
room_id1,
event_type=EventTypes.Member,
state_key=user1_id,
body={
"membership": Membership.JOIN,
"displayname": "displayname during token range",
},
tok=user1_tok,
)
# Update the displayname during the token range (looks like another join)
displayname_change_during_token_range_response2 = self.helper.send_state(
room_id1,
event_type=EventTypes.Member,
state_key=user1_id,
body={
"membership": Membership.JOIN,
"displayname": "displayname during token range",
},
tok=user1_tok,
)
after_room1_token = self.event_sources.get_current_token()
room_id_results = self.get_success(
self.sliding_sync_handler.get_sync_room_ids_for_user(
UserID.from_string(user1_id),
from_token=before_room1_token,
to_token=after_room1_token,
)
)
# Room should show up because it was newly_left and joined during the from/to range
self.assertEqual(room_id_results.keys(), {room_id1})
# It should be pointing to the latest membership event in the from/to range
self.assertEqual(
room_id_results[room_id1].event_id,
displayname_change_during_token_range_response2["event_id"],
"Corresponding map to disambiguate the opaque event IDs: "
+ str(
{
"join_response1": join_response1["event_id"],
"displayname_change_during_token_range_response1": displayname_change_during_token_range_response1[
"event_id"
],
"displayname_change_during_token_range_response2": displayname_change_during_token_range_response2[
"event_id"
],
}
),
)
# We should be `newly_joined` because we first joined during the token range
self.assertEqual(room_id_results[room_id1].newly_joined, True)
def test_multiple_rooms_are_not_confused(
self,
) -> None: