Repository: matrix-org/synapse

Issue:
event_id [...] is not related to thread main when sending a read receipt for the "main" thread
From @gileluard:

> I'm still on read receipts for threads and I receive the following error when sending a RR on the main thread
>
> error=event_id $... is not related to thread main

I think we might need some special logic for the main thread in `get_thread_id`


Discussion:
Just retested it and on main thread we receive an error:
`[POST] _matrix/client/r0/rooms/!ekRLBNvlGiHnceBzQq:threads-dev.lab.element.dev/receipt/m.read/%24j-4JjonTway1BTy2G_rCOSSC2uxrp_8Bxi3Qyum9Q-I`
Parameters
```
{
    "thread_id": "main"
}
```
-> Failure
```
Error Domain=org.matrix.sdk Code=6 "event_id $j-4JjonTway1BTy2G_rCOSSC2uxrp_8Bxi3Qyum9Q-I is not related to thread main" UserInfo={errcode=M_INVALID_PARAM, httpResponse=<NSHTTPURLResponse: 0x6000007c2600> { URL: https://threads-dev.lab.element.dev/_matrix/client/r0/rooms/!ekRLBNvlGiHnceBzQq:threads-dev.lab.element.dev/receipt/m.read/%24j-4JjonTway1BTy2G_rCOSSC2uxrp_8Bxi3Qyum9Q-I } { Status Code: 400, Headers {
    "Access-Control-Allow-Origin" =     (
        "*"
    );
    "Cache-Control" =     (
        "no-cache, no-store, must-revalidate"
    );
    "Content-Type" =     (
        "application/json"
    );
    Date =     (
        "Tue, 11 Oct 2022 13:00:44 GMT"
    );
    Server =     (
        "Synapse/1.68.0rc1"
    );
    "Transfer-Encoding" =     (
        Identity
    );
    "access-control-allow-headers" =     (
        "X-Requested-With, Content-Type, Authorization, Date"
    );
    "access-control-allow-methods" =     (
        "GET, HEAD, POST, PUT, DELETE, OPTIONS"
    );
    "permissions-policy" =     (
        "interest-cohort=()"
    );
} }, error=event_id $j-4JjonTway1BTy2G_rCOSSC2uxrp_8Bxi3Qyum9Q-I is not related to thread main, NSLocalizedDescription=event_id $j-4JjonTway1BTy2G_rCOSSC2uxrp_8Bxi3Qyum9Q-I is not related to thread main}
```

Note that the request is successful if the read receipt is sent on a specific thread using the same method.

Resolving patch:
diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py
--- a/synapse/push/bulk_push_rule_evaluator.py
+++ b/synapse/push/bulk_push_rule_evaluator.py
@@ -292,7 +292,7 @@ async def action_for_event_by_user(
             else:
                 # Since the event has not yet been persisted we check whether
                 # the parent is part of a thread.
-                thread_id = await self.store.get_thread_id(relation.parent_id) or "main"
+                thread_id = await self.store.get_thread_id(relation.parent_id)
 
         # It's possible that old room versions have non-integer power levels (floats or
         # strings). Workaround this by explicitly converting to int.
diff --git a/synapse/storage/databases/main/relations.py b/synapse/storage/databases/main/relations.py
--- a/synapse/storage/databases/main/relations.py
+++ b/synapse/storage/databases/main/relations.py
@@ -28,7 +28,7 @@
 
 import attr
 
-from synapse.api.constants import RelationTypes
+from synapse.api.constants import MAIN_TIMELINE, RelationTypes
 from synapse.events import EventBase
 from synapse.storage._base import SQLBaseStore
 from synapse.storage.database import LoggingTransaction, make_in_list_sql_clause
@@ -830,7 +830,7 @@ def _get_event_relations(
         )
 
     @cached()
-    async def get_thread_id(self, event_id: str) -> Optional[str]:
+    async def get_thread_id(self, event_id: str) -> str:
         """
         Get the thread ID for an event. This considers multi-level relations,
         e.g. an annotation to an event which is part of a thread.
@@ -840,7 +840,7 @@ async def get_thread_id(self, event_id: str) -> Optional[str]:
 
         Returns:
             The event ID of the root event in the thread, if this event is part
-            of a thread. None, otherwise.
+            of a thread. "main", otherwise.
         """
         # Since event relations form a tree, we should only ever find 0 or 1
         # results from the below query.
@@ -855,13 +855,15 @@ async def get_thread_id(self, event_id: str) -> Optional[str]:
             ) SELECT relates_to_id FROM related_events WHERE relation_type = 'm.thread';
         """
 
-        def _get_thread_id(txn: LoggingTransaction) -> Optional[str]:
+        def _get_thread_id(txn: LoggingTransaction) -> str:
             txn.execute(sql, (event_id,))
             # TODO Should we ensure there's only a single result here?
             row = txn.fetchone()
             if row:
                 return row[0]
-            return None
+
+            # If no thread was found, it is part of the main timeline.
+            return MAIN_TIMELINE
 
         return await self.db_pool.runInteraction("get_thread_id", _get_thread_id)
 


Test patch: