diff --git a/sync3/handler/connstate.go b/sync3/handler/connstate.go index c6a39378..974bafc6 100644 --- a/sync3/handler/connstate.go +++ b/sync3/handler/connstate.go @@ -465,6 +465,12 @@ func (s *ConnState) buildRooms(ctx context.Context, builtSubs []BuiltSubscriptio ctx, span := internal.StartSpan(ctx, "buildRooms") defer span.End() result := make(map[string]sync3.Room) + + var bumpEventTypes []string + for _, x := range s.muxedReq.Lists { + bumpEventTypes = append(bumpEventTypes, x.BumpEventTypes...) + } + for _, bs := range builtSubs { roomIDs := bs.RoomIDs if bs.RoomSubscription.IncludeOldRooms != nil { @@ -493,7 +499,7 @@ func (s *ConnState) buildRooms(ctx context.Context, builtSubs []BuiltSubscriptio // If we have old rooms to fetch, do so. if len(oldRoomIDs) > 0 { // old rooms use a different subscription - oldRooms := s.getInitialRoomData(ctx, *bs.RoomSubscription.IncludeOldRooms, oldRoomIDs...) + oldRooms := s.getInitialRoomData(ctx, *bs.RoomSubscription.IncludeOldRooms, bumpEventTypes, oldRoomIDs...) for oldRoomID, oldRoom := range oldRooms { result[oldRoomID] = oldRoom } @@ -505,7 +511,7 @@ func (s *ConnState) buildRooms(ctx context.Context, builtSubs []BuiltSubscriptio continue } - rooms := s.getInitialRoomData(ctx, bs.RoomSubscription, roomIDs...) + rooms := s.getInitialRoomData(ctx, bs.RoomSubscription, bumpEventTypes, roomIDs...) for roomID, room := range rooms { result[roomID] = room } @@ -542,7 +548,7 @@ func (s *ConnState) lazyLoadTypingMembers(ctx context.Context, response *sync3.R } } -func (s *ConnState) getInitialRoomData(ctx context.Context, roomSub sync3.RoomSubscription, roomIDs ...string) map[string]sync3.Room { +func (s *ConnState) getInitialRoomData(ctx context.Context, roomSub sync3.RoomSubscription, bumpEventTypes []string, roomIDs ...string) map[string]sync3.Room { ctx, span := internal.StartSpan(ctx, "getInitialRoomData") defer span.End() rooms := make(map[string]sync3.Room, len(roomIDs)) @@ -611,6 +617,31 @@ func (s *ConnState) getInitialRoomData(ctx context.Context, roomSub sync3.RoomSu requiredState = make([]json.RawMessage, 0) } } + + // Get the highest timestamp, determined by bumpEventTypes, + // for this room + roomListsMeta := s.lists.ReadOnlyRoom(roomID) + var maxTs uint64 + for _, t := range bumpEventTypes { + if roomListsMeta == nil { + break + } + + evMeta := roomListsMeta.LatestEventsByType[t] + if evMeta.Timestamp > maxTs { + maxTs = evMeta.Timestamp + } + } + + // If we didn't find any events which would update the timestamp + // use the join event timestamp instead. Also don't leak + // timestamp from before we joined. + if maxTs == 0 || maxTs < roomListsMeta.JoinTiming.Timestamp { + if roomListsMeta != nil { + maxTs = roomListsMeta.JoinTiming.Timestamp + } + } + rooms[roomID] = sync3.Room{ Name: internal.CalculateRoomName(metadata, 5), // TODO: customisable? AvatarChange: sync3.NewAvatarChange(internal.CalculateAvatar(metadata)), @@ -624,6 +655,7 @@ func (s *ConnState) getInitialRoomData(ctx context.Context, roomSub sync3.RoomSu JoinedCount: metadata.JoinCount, InvitedCount: &metadata.InviteCount, PrevBatch: userRoomData.RequestedLatestEvents.PrevBatch, + Timestamp: maxTs, } } diff --git a/sync3/handler/connstate_live.go b/sync3/handler/connstate_live.go index c8d12f7e..13896a5e 100644 --- a/sync3/handler/connstate_live.go +++ b/sync3/handler/connstate_live.go @@ -187,6 +187,30 @@ func (s *connStateLive) processLiveUpdate(ctx context.Context, up caches.Update, // include this update in the rooms response TODO: filters on event type? userRoomData := roomUpdate.UserRoomMetadata() r := response.Rooms[roomUpdate.RoomID()] + + // Get the highest timestamp, determined by bumpEventTypes, + // for this room + roomListsMeta := s.lists.ReadOnlyRoom(roomUpdate.RoomID()) + var bumpEventTypes []string + for _, list := range s.muxedReq.Lists { + bumpEventTypes = append(bumpEventTypes, list.BumpEventTypes...) + } + for _, t := range bumpEventTypes { + evMeta := roomListsMeta.LatestEventsByType[t] + if evMeta.Timestamp > r.Timestamp { + r.Timestamp = evMeta.Timestamp + } + } + + // If there are no bumpEventTypes defined, use the last message timestamp + if r.Timestamp == 0 && len(bumpEventTypes) == 0 { + r.Timestamp = roomUpdate.GlobalRoomMetadata().LastMessageTimestamp + } + // Make sure we don't leak a timestamp from before we joined + if r.Timestamp < roomListsMeta.JoinTiming.Timestamp { + r.Timestamp = roomListsMeta.JoinTiming.Timestamp + } + r.HighlightCount = int64(userRoomData.HighlightCount) r.NotificationCount = int64(userRoomData.NotificationCount) if roomEventUpdate != nil && roomEventUpdate.EventData.Event != nil { diff --git a/sync3/room.go b/sync3/room.go index 3e400982..53086082 100644 --- a/sync3/room.go +++ b/sync3/room.go @@ -22,6 +22,7 @@ type Room struct { InvitedCount *int `json:"invited_count,omitempty"` PrevBatch string `json:"prev_batch,omitempty"` NumLive int `json:"num_live,omitempty"` + Timestamp uint64 `json:"timestamp,omitempty"` } // RoomConnMetadata represents a room as seen by one specific connection (hence one diff --git a/tests-e2e/timestamp_test.go b/tests-e2e/timestamp_test.go new file mode 100644 index 00000000..ef180f52 --- /dev/null +++ b/tests-e2e/timestamp_test.go @@ -0,0 +1,166 @@ +package syncv3_test + +import ( + "testing" + "time" + + "github.com/matrix-org/sliding-sync/sync3" + "github.com/matrix-org/sliding-sync/testutils/m" +) + +func TestTimestamp(t *testing.T) { + alice := registerNewUser(t) + bob := registerNewUser(t) + charlie := registerNewUser(t) + + roomID := alice.CreateRoom(t, map[string]interface{}{ + "preset": "public_chat", + }) + + var gotTs, expectedTs uint64 + + lists := map[string]sync3.RequestList{ + "myFirstList": { + Ranges: [][2]int64{{0, 1}}, + RoomSubscription: sync3.RoomSubscription{ + TimelineLimit: 10, + }, + BumpEventTypes: []string{"m.room.message"}, // only messages bump the timestamp + }, + "mySecondList": { + Ranges: [][2]int64{{0, 1}}, + RoomSubscription: sync3.RoomSubscription{ + TimelineLimit: 10, + }, + BumpEventTypes: []string{"m.reaction"}, // only reactions bump the timestamp + }, + } + + // Init sync to get the latest timestamp + resAlice := alice.SlidingSync(t, sync3.Request{ + RoomSubscriptions: map[string]sync3.RoomSubscription{ + roomID: { + TimelineLimit: 10, + }, + }, + }) + m.MatchResponse(t, resAlice, m.MatchRoomSubscription(roomID, m.MatchRoomInitial(true))) + timestampBeforeBobJoined := resAlice.Rooms[roomID].Timestamp + + bob.JoinRoom(t, roomID, nil) + resAlice = alice.SlidingSyncUntilMembership(t, resAlice.Pos, roomID, bob, "join") + resBob := bob.SlidingSync(t, sync3.Request{ + Lists: lists, + }) + + // Bob should see a different timestamp than alice, as he just joined + gotTs = resBob.Rooms[roomID].Timestamp + expectedTs = resAlice.Rooms[roomID].Timestamp + if gotTs != expectedTs { + t.Fatalf("expected timestamp to be equal, but got: %v vs %v", gotTs, expectedTs) + } + // ... the timestamp should still differ from what Alice received before the join + if gotTs == timestampBeforeBobJoined { + t.Fatalf("expected timestamp to differ, but got: %v vs %v", gotTs, timestampBeforeBobJoined) + } + + // Send an event which should NOT bump Bobs timestamp, because it is not listed it + // any BumpEventTypes + emptyStateKey := "" + eventID := alice.SendEventSynced(t, roomID, Event{ + Type: "m.room.topic", + StateKey: &emptyStateKey, + Content: map[string]interface{}{ + "topic": "random topic", + }, + }) + time.Sleep(time.Millisecond) + + resBob = bob.SlidingSyncUntilEventID(t, resBob.Pos, roomID, eventID) + gotTs = resBob.Rooms[roomID].Timestamp + expectedTs = resAlice.Rooms[roomID].Timestamp + if gotTs != expectedTs { + t.Fatalf("expected timestamps to be the same, but they aren't: %v vs %v", gotTs, expectedTs) + } + expectedTs = gotTs + + // Now send a message which bumps the timestamp in myFirstList + eventID = alice.SendEventSynced(t, roomID, Event{ + Type: "m.room.message", + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "Hello, world!", + }, + }) + time.Sleep(time.Millisecond) + + resBob = bob.SlidingSyncUntilEventID(t, resBob.Pos, roomID, eventID) + gotTs = resBob.Rooms[roomID].Timestamp + if expectedTs == gotTs { + t.Fatalf("expected timestamps to be different, but they aren't: %v vs %v", gotTs, expectedTs) + } + expectedTs = gotTs + + // Now send a message which bumps the timestamp in mySecondList + eventID = alice.SendEventSynced(t, roomID, Event{ + Type: "m.reaction", + Content: map[string]interface{}{ + "m.relates.to": map[string]interface{}{ + "event_id": eventID, + "key": "✅", + "rel_type": "m.annotation", + }, + }, + }) + time.Sleep(time.Millisecond) + + resBob = bob.SlidingSyncUntilEventID(t, resBob.Pos, roomID, eventID) + bobTimestampReaction := resBob.Rooms[roomID].Timestamp + if bobTimestampReaction == expectedTs { + t.Fatalf("expected timestamps to be different, but they aren't: %v vs %v", expectedTs, bobTimestampReaction) + } + expectedTs = bobTimestampReaction + + // Send another event which should NOT bump Bobs timestamp + eventID = alice.SendEventSynced(t, roomID, Event{ + Type: "m.room.name", + StateKey: &emptyStateKey, + Content: map[string]interface{}{ + "name": "random name", + }, + }) + time.Sleep(time.Millisecond) + + resBob = bob.SlidingSyncUntilEventID(t, resBob.Pos, roomID, eventID) + gotTs = resBob.Rooms[roomID].Timestamp + if gotTs != expectedTs { + t.Fatalf("expected timestamps to be the same, but they aren't: %v, expected %v", gotTs, expectedTs) + } + + // Bob makes an initial sync again, he should still see the m.reaction timestamp + resBob = bob.SlidingSync(t, sync3.Request{ + Lists: lists, + }) + + gotTs = resBob.Rooms[roomID].Timestamp + expectedTs = bobTimestampReaction + if gotTs != expectedTs { + t.Fatalf("initial sync contains wrong timestamp: %d, expected %d", gotTs, expectedTs) + } + + // Charlie joins the room + charlie.JoinRoom(t, roomID, nil) + resAlice = alice.SlidingSyncUntilMembership(t, resAlice.Pos, roomID, charlie, "join") + + resCharlie := charlie.SlidingSync(t, sync3.Request{ + Lists: lists, + }) + + // Charlie just joined so should see the same timestamp as Alice, even if + // Charlie has the same bumpEvents as Bob, we don't leak those timestamps. + gotTs = resCharlie.Rooms[roomID].Timestamp + expectedTs = resAlice.Rooms[roomID].Timestamp + if gotTs != expectedTs { + t.Fatalf("Charlie should see the timestamp they joined, but didn't: %d, expected %d", gotTs, expectedTs) + } +}