Repository: matrix-org/synapse

Issue:
Implement MSC3856, a threads list API
Somehow vector-im/element-meta#152 got morphed into a Synapse specific issue and vector-im/element-meta#195 was another meta issue that was filed, let's file an actual Synapse issue for this:

----

[MSC3856](https://github.com/matrix-org/matrix-spec-proposals/pull/3856) describes a threads list endpoint for clients to be able to query for the most up-to-date threads in a room.


Discussion:

Resolving patch:
diff --git a/docker/configure_workers_and_start.py b/docker/configure_workers_and_start.py
--- a/docker/configure_workers_and_start.py
+++ b/docker/configure_workers_and_start.py
@@ -118,6 +118,7 @@
             "^/_matrix/client/(api/v1|r0|v3|unstable)/rooms/.*/state$",
             "^/_matrix/client/v1/rooms/.*/hierarchy$",
             "^/_matrix/client/(v1|unstable)/rooms/.*/relations/",
+            "^/_matrix/client/v1/rooms/.*/threads$",
             "^/_matrix/client/(api/v1|r0|v3|unstable)/login$",
             "^/_matrix/client/(api/v1|r0|v3|unstable)/account/3pid$",
             "^/_matrix/client/(api/v1|r0|v3|unstable)/account/whoami$",
diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -101,9 +101,6 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
         # MSC3848: Introduce errcodes for specific event sending failures
         self.msc3848_enabled: bool = experimental.get("msc3848_enabled", False)
 
-        # MSC3856: Threads list API
-        self.msc3856_enabled: bool = experimental.get("msc3856_enabled", False)
-
         # MSC3852: Expose last seen user agent field on /_matrix/client/v3/devices.
         self.msc3852_enabled: bool = experimental.get("msc3852_enabled", False)
 
diff --git a/synapse/rest/client/relations.py b/synapse/rest/client/relations.py
--- a/synapse/rest/client/relations.py
+++ b/synapse/rest/client/relations.py
@@ -82,11 +82,7 @@ async def on_GET(
 
 
 class ThreadsServlet(RestServlet):
-    PATTERNS = (
-        re.compile(
-            "^/_matrix/client/unstable/org.matrix.msc3856/rooms/(?P<room_id>[^/]*)/threads"
-        ),
-    )
+    PATTERNS = (re.compile("^/_matrix/client/v1/rooms/(?P<room_id>[^/]*)/threads"),)
 
     def __init__(self, hs: "HomeServer"):
         super().__init__()
@@ -126,5 +122,4 @@ async def on_GET(
 
 def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
     RelationPaginationServlet(hs).register(http_server)
-    if hs.config.experimental.msc3856_enabled:
-        ThreadsServlet(hs).register(http_server)
+    ThreadsServlet(hs).register(http_server)


Test patch:
diff --git a/tests/rest/client/test_relations.py b/tests/rest/client/test_relations.py
--- a/tests/rest/client/test_relations.py
+++ b/tests/rest/client/test_relations.py
@@ -1710,7 +1710,15 @@ def test_redact_parent_thread(self) -> None:
 
 
 class ThreadsTestCase(BaseRelationsTestCase):
-    @unittest.override_config({"experimental_features": {"msc3856_enabled": True}})
+    def _get_threads(self, body: JsonDict) -> List[Tuple[str, str]]:
+        return [
+            (
+                ev["event_id"],
+                ev["unsigned"]["m.relations"]["m.thread"]["latest_event"]["event_id"],
+            )
+            for ev in body["chunk"]
+        ]
+
     def test_threads(self) -> None:
         """Create threads and ensure the ordering is due to their latest event."""
         # Create 2 threads.
@@ -1718,32 +1726,37 @@ def test_threads(self) -> None:
         res = self.helper.send(self.room, body="Thread Root!", tok=self.user_token)
         thread_2 = res["event_id"]
 
-        self._send_relation(RelationTypes.THREAD, "m.room.test")
-        self._send_relation(RelationTypes.THREAD, "m.room.test", parent_id=thread_2)
+        channel = self._send_relation(RelationTypes.THREAD, "m.room.test")
+        reply_1 = channel.json_body["event_id"]
+        channel = self._send_relation(
+            RelationTypes.THREAD, "m.room.test", parent_id=thread_2
+        )
+        reply_2 = channel.json_body["event_id"]
 
         # Request the threads in the room.
         channel = self.make_request(
             "GET",
-            f"/_matrix/client/unstable/org.matrix.msc3856/rooms/{self.room}/threads",
+            f"/_matrix/client/v1/rooms/{self.room}/threads",
             access_token=self.user_token,
         )
         self.assertEquals(200, channel.code, channel.json_body)
-        thread_roots = [ev["event_id"] for ev in channel.json_body["chunk"]]
-        self.assertEqual(thread_roots, [thread_2, thread_1])
+        threads = self._get_threads(channel.json_body)
+        self.assertEqual(threads, [(thread_2, reply_2), (thread_1, reply_1)])
 
         # Update the first thread, the ordering should swap.
-        self._send_relation(RelationTypes.THREAD, "m.room.test")
+        channel = self._send_relation(RelationTypes.THREAD, "m.room.test")
+        reply_3 = channel.json_body["event_id"]
 
         channel = self.make_request(
             "GET",
-            f"/_matrix/client/unstable/org.matrix.msc3856/rooms/{self.room}/threads",
+            f"/_matrix/client/v1/rooms/{self.room}/threads",
             access_token=self.user_token,
         )
         self.assertEquals(200, channel.code, channel.json_body)
-        thread_roots = [ev["event_id"] for ev in channel.json_body["chunk"]]
-        self.assertEqual(thread_roots, [thread_1, thread_2])
+        # Tuple of (thread ID, latest event ID) for each thread.
+        threads = self._get_threads(channel.json_body)
+        self.assertEqual(threads, [(thread_1, reply_3), (thread_2, reply_2)])
 
-    @unittest.override_config({"experimental_features": {"msc3856_enabled": True}})
     def test_pagination(self) -> None:
         """Create threads and paginate through them."""
         # Create 2 threads.
@@ -1757,7 +1770,7 @@ def test_pagination(self) -> None:
         # Request the threads in the room.
         channel = self.make_request(
             "GET",
-            f"/_matrix/client/unstable/org.matrix.msc3856/rooms/{self.room}/threads?limit=1",
+            f"/_matrix/client/v1/rooms/{self.room}/threads?limit=1",
             access_token=self.user_token,
         )
         self.assertEquals(200, channel.code, channel.json_body)
@@ -1771,7 +1784,7 @@ def test_pagination(self) -> None:
 
         channel = self.make_request(
             "GET",
-            f"/_matrix/client/unstable/org.matrix.msc3856/rooms/{self.room}/threads?limit=1&from={next_batch}",
+            f"/_matrix/client/v1/rooms/{self.room}/threads?limit=1&from={next_batch}",
             access_token=self.user_token,
         )
         self.assertEquals(200, channel.code, channel.json_body)
@@ -1780,7 +1793,6 @@ def test_pagination(self) -> None:
 
         self.assertNotIn("next_batch", channel.json_body, channel.json_body)
 
-    @unittest.override_config({"experimental_features": {"msc3856_enabled": True}})
     def test_include(self) -> None:
         """Filtering threads to all or participated in should work."""
         # Thread 1 has the user as the root event.
@@ -1807,7 +1819,7 @@ def test_include(self) -> None:
         # All threads in the room.
         channel = self.make_request(
             "GET",
-            f"/_matrix/client/unstable/org.matrix.msc3856/rooms/{self.room}/threads",
+            f"/_matrix/client/v1/rooms/{self.room}/threads",
             access_token=self.user_token,
         )
         self.assertEquals(200, channel.code, channel.json_body)
@@ -1819,14 +1831,13 @@ def test_include(self) -> None:
         # Only participated threads.
         channel = self.make_request(
             "GET",
-            f"/_matrix/client/unstable/org.matrix.msc3856/rooms/{self.room}/threads?include=participated",
+            f"/_matrix/client/v1/rooms/{self.room}/threads?include=participated",
             access_token=self.user_token,
         )
         self.assertEquals(200, channel.code, channel.json_body)
         thread_roots = [ev["event_id"] for ev in channel.json_body["chunk"]]
         self.assertEqual(thread_roots, [thread_2, thread_1], channel.json_body)
 
-    @unittest.override_config({"experimental_features": {"msc3856_enabled": True}})
     def test_ignored_user(self) -> None:
         """Events from ignored users should be ignored."""
         # Thread 1 has a reply from an ignored user.
@@ -1852,7 +1863,7 @@ def test_ignored_user(self) -> None:
         # Only thread 1 is returned.
         channel = self.make_request(
             "GET",
-            f"/_matrix/client/unstable/org.matrix.msc3856/rooms/{self.room}/threads",
+            f"/_matrix/client/v1/rooms/{self.room}/threads",
             access_token=self.user_token,
         )
         self.assertEquals(200, channel.code, channel.json_body)