From 52f456a8228eaf4d6cc75c0514788e8507975035 Mon Sep 17 00:00:00 2001
From: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
Date: Thu, 14 Mar 2024 17:34:19 +0000
Subject: [PATCH] `/sync`: Fix edge-case in calculating the "device_lists"
 response (#16949)

Fixes https://github.com/element-hq/synapse/issues/16948. If the `join`
and the `leave` are in the same sync response, we need to count them as
a "left" user.
---
 changelog.d/16949.bugfix |  1 +
 synapse/handlers/sync.py | 11 +++++++++--
 2 files changed, 10 insertions(+), 2 deletions(-)
 create mode 100644 changelog.d/16949.bugfix

diff --git a/changelog.d/16949.bugfix b/changelog.d/16949.bugfix
new file mode 100644
index 0000000000..99ed435d75
--- /dev/null
+++ b/changelog.d/16949.bugfix
@@ -0,0 +1 @@
+Fix various long-standing bugs which could cause incorrect state to be returned from `/sync` in certain situations.
diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py
index 08fe4eb3b3..0aedb37f16 100644
--- a/synapse/handlers/sync.py
+++ b/synapse/handlers/sync.py
@@ -2837,7 +2837,7 @@ class SyncResultBuilder:
         if self.since_token:
             for joined_sync in self.joined:
                 it = itertools.chain(
-                    joined_sync.timeline.events, joined_sync.state.values()
+                    joined_sync.state.values(), joined_sync.timeline.events
                 )
                 for event in it:
                     if event.type == EventTypes.Member:
@@ -2849,13 +2849,20 @@ class SyncResultBuilder:
                             newly_joined_or_invited_or_knocked_users.add(
                                 event.state_key
                             )
+                            # If the user left and rejoined in the same batch, they
+                            # count as a newly-joined user, *not* a newly-left user.
+                            newly_left_users.discard(event.state_key)
                         else:
                             prev_content = event.unsigned.get("prev_content", {})
                             prev_membership = prev_content.get("membership", None)
                             if prev_membership == Membership.JOIN:
                                 newly_left_users.add(event.state_key)
+                            # If the user joined and left in the same batch, they
+                            # count as a newly-left user, not a newly-joined user.
+                            newly_joined_or_invited_or_knocked_users.discard(
+                                event.state_key
+                            )
 
-        newly_left_users -= newly_joined_or_invited_or_knocked_users
         return newly_joined_or_invited_or_knocked_users, newly_left_users