Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 3bbe532

Browse files
authored
Add an API for listing threads in a room. (#13394)
Implement the /threads endpoint from MSC3856. This is currently unstable and behind an experimental configuration flag. It includes a background update to backfill data, results from the /threads endpoint will be partial until that finishes.
1 parent b6baa46 commit 3bbe532

File tree

10 files changed

+522
-6
lines changed

10 files changed

+522
-6
lines changed

changelog.d/13394.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Experimental support for [MSC3856](https://github.com/matrix-org/matrix-spec-proposals/pull/3856): threads list API.

synapse/_scripts/synapse_port_db.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
RegistrationBackgroundUpdateStore,
7373
find_max_generated_user_id_localpart,
7474
)
75+
from synapse.storage.databases.main.relations import RelationsWorkerStore
7576
from synapse.storage.databases.main.room import RoomBackgroundUpdateStore
7677
from synapse.storage.databases.main.roommember import RoomMemberBackgroundUpdateStore
7778
from synapse.storage.databases.main.search import SearchBackgroundUpdateStore
@@ -206,6 +207,7 @@ class Store(
206207
PusherWorkerStore,
207208
PresenceBackgroundUpdateStore,
208209
ReceiptsBackgroundUpdateStore,
210+
RelationsWorkerStore,
209211
):
210212
def execute(self, f: Callable[..., R], *args: Any, **kwargs: Any) -> Awaitable[R]:
211213
return self.db_pool.runInteraction(f.__name__, f, *args, **kwargs)

synapse/config/experimental.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
101101
# MSC3848: Introduce errcodes for specific event sending failures
102102
self.msc3848_enabled: bool = experimental.get("msc3848_enabled", False)
103103

104+
# MSC3856: Threads list API
105+
self.msc3856_enabled: bool = experimental.get("msc3856_enabled", False)
106+
104107
# MSC3852: Expose last seen user agent field on /_matrix/client/v3/devices.
105108
self.msc3852_enabled: bool = experimental.get("msc3852_enabled", False)
106109

synapse/handlers/relations.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14+
import enum
1415
import logging
1516
from typing import TYPE_CHECKING, Dict, FrozenSet, Iterable, List, Optional, Tuple
1617

@@ -20,7 +21,7 @@
2021
from synapse.api.errors import SynapseError
2122
from synapse.events import EventBase, relation_from_event
2223
from synapse.logging.opentracing import trace
23-
from synapse.storage.databases.main.relations import _RelatedEvent
24+
from synapse.storage.databases.main.relations import ThreadsNextBatch, _RelatedEvent
2425
from synapse.streams.config import PaginationConfig
2526
from synapse.types import JsonDict, Requester, StreamToken, UserID
2627
from synapse.visibility import filter_events_for_client
@@ -32,6 +33,13 @@
3233
logger = logging.getLogger(__name__)
3334

3435

36+
class ThreadsListInclude(str, enum.Enum):
37+
"""Valid values for the 'include' flag of /threads."""
38+
39+
all = "all"
40+
participated = "participated"
41+
42+
3543
@attr.s(slots=True, frozen=True, auto_attribs=True)
3644
class _ThreadAggregation:
3745
# The latest event in the thread.
@@ -482,3 +490,79 @@ async def get_bundled_aggregations(
482490
results.setdefault(event_id, BundledAggregations()).replace = edit
483491

484492
return results
493+
494+
async def get_threads(
495+
self,
496+
requester: Requester,
497+
room_id: str,
498+
include: ThreadsListInclude,
499+
limit: int = 5,
500+
from_token: Optional[ThreadsNextBatch] = None,
501+
) -> JsonDict:
502+
"""Get related events of a event, ordered by topological ordering.
503+
504+
Args:
505+
requester: The user requesting the relations.
506+
room_id: The room the event belongs to.
507+
include: One of "all" or "participated" to indicate which threads should
508+
be returned.
509+
limit: Only fetch the most recent `limit` events.
510+
from_token: Fetch rows from the given token, or from the start if None.
511+
512+
Returns:
513+
The pagination chunk.
514+
"""
515+
516+
user_id = requester.user.to_string()
517+
518+
# TODO Properly handle a user leaving a room.
519+
(_, member_event_id) = await self._auth.check_user_in_room_or_world_readable(
520+
room_id, requester, allow_departed_users=True
521+
)
522+
523+
# Note that ignored users are not passed into get_relations_for_event
524+
# below. Ignored users are handled in filter_events_for_client (and by
525+
# not passing them in here we should get a better cache hit rate).
526+
thread_roots, next_batch = await self._main_store.get_threads(
527+
room_id=room_id, limit=limit, from_token=from_token
528+
)
529+
530+
events = await self._main_store.get_events_as_list(thread_roots)
531+
532+
if include == ThreadsListInclude.participated:
533+
# Pre-seed thread participation with whether the requester sent the event.
534+
participated = {event.event_id: event.sender == user_id for event in events}
535+
# For events the requester did not send, check the database for whether
536+
# the requester sent a threaded reply.
537+
participated.update(
538+
await self._main_store.get_threads_participated(
539+
[eid for eid, p in participated.items() if not p],
540+
user_id,
541+
)
542+
)
543+
544+
# Limit the returned threads to those the user has participated in.
545+
events = [event for event in events if participated[event.event_id]]
546+
547+
events = await filter_events_for_client(
548+
self._storage_controllers,
549+
user_id,
550+
events,
551+
is_peeking=(member_event_id is None),
552+
)
553+
554+
aggregations = await self.get_bundled_aggregations(
555+
events, requester.user.to_string()
556+
)
557+
558+
now = self._clock.time_msec()
559+
serialized_events = self._event_serializer.serialize_events(
560+
events, now, bundle_aggregations=aggregations
561+
)
562+
563+
return_value: JsonDict = {"chunk": serialized_events}
564+
565+
if next_batch:
566+
return_value["next_batch"] = str(next_batch)
567+
568+
return return_value

synapse/rest/client/relations.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
# limitations under the License.
1414

1515
import logging
16+
import re
1617
from typing import TYPE_CHECKING, Optional, Tuple
1718

19+
from synapse.handlers.relations import ThreadsListInclude
1820
from synapse.http.server import HttpServer
19-
from synapse.http.servlet import RestServlet
21+
from synapse.http.servlet import RestServlet, parse_integer, parse_string
2022
from synapse.http.site import SynapseRequest
2123
from synapse.rest.client._base import client_patterns
24+
from synapse.storage.databases.main.relations import ThreadsNextBatch
2225
from synapse.streams.config import PaginationConfig
2326
from synapse.types import JsonDict
2427

@@ -78,5 +81,50 @@ async def on_GET(
7881
return 200, result
7982

8083

84+
class ThreadsServlet(RestServlet):
85+
PATTERNS = (
86+
re.compile(
87+
"^/_matrix/client/unstable/org.matrix.msc3856/rooms/(?P<room_id>[^/]*)/threads"
88+
),
89+
)
90+
91+
def __init__(self, hs: "HomeServer"):
92+
super().__init__()
93+
self.auth = hs.get_auth()
94+
self.store = hs.get_datastores().main
95+
self._relations_handler = hs.get_relations_handler()
96+
97+
async def on_GET(
98+
self, request: SynapseRequest, room_id: str
99+
) -> Tuple[int, JsonDict]:
100+
requester = await self.auth.get_user_by_req(request)
101+
102+
limit = parse_integer(request, "limit", default=5)
103+
from_token_str = parse_string(request, "from")
104+
include = parse_string(
105+
request,
106+
"include",
107+
default=ThreadsListInclude.all.value,
108+
allowed_values=[v.value for v in ThreadsListInclude],
109+
)
110+
111+
# Return the relations
112+
from_token = None
113+
if from_token_str:
114+
from_token = ThreadsNextBatch.from_string(from_token_str)
115+
116+
result = await self._relations_handler.get_threads(
117+
requester=requester,
118+
room_id=room_id,
119+
include=ThreadsListInclude(include),
120+
limit=limit,
121+
from_token=from_token,
122+
)
123+
124+
return 200, result
125+
126+
81127
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
82128
RelationPaginationServlet(hs).register(http_server)
129+
if hs.config.experimental.msc3856_enabled:
130+
ThreadsServlet(hs).register(http_server)

synapse/storage/databases/main/cache.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ def _invalidate_caches_for_event(
259259
self._attempt_to_invalidate_cache("get_applicable_edit", (relates_to,))
260260
self._attempt_to_invalidate_cache("get_thread_summary", (relates_to,))
261261
self._attempt_to_invalidate_cache("get_thread_participated", (relates_to,))
262+
self._attempt_to_invalidate_cache("get_threads", (room_id,))
262263

263264
async def invalidate_cache_and_stream(
264265
self, cache_name: str, keys: Tuple[Any, ...]

synapse/storage/databases/main/events.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from prometheus_client import Counter
3636

3737
import synapse.metrics
38-
from synapse.api.constants import EventContentFields, EventTypes
38+
from synapse.api.constants import EventContentFields, EventTypes, RelationTypes
3939
from synapse.api.errors import Codes, SynapseError
4040
from synapse.api.room_versions import RoomVersions
4141
from synapse.events import EventBase, relation_from_event
@@ -1616,7 +1616,7 @@ def _update_metadata_tables_txn(
16161616
)
16171617

16181618
# Remove from relations table.
1619-
self._handle_redact_relations(txn, event.redacts)
1619+
self._handle_redact_relations(txn, event.room_id, event.redacts)
16201620

16211621
# Update the event_forward_extremities, event_backward_extremities and
16221622
# event_edges tables.
@@ -1866,6 +1866,34 @@ def _handle_event_relations(
18661866
},
18671867
)
18681868

1869+
if relation.rel_type == RelationTypes.THREAD:
1870+
# Upsert into the threads table, but only overwrite the value if the
1871+
# new event is of a later topological order OR if the topological
1872+
# ordering is equal, but the stream ordering is later.
1873+
sql = """
1874+
INSERT INTO threads (room_id, thread_id, latest_event_id, topological_ordering, stream_ordering)
1875+
VALUES (?, ?, ?, ?, ?)
1876+
ON CONFLICT (room_id, thread_id)
1877+
DO UPDATE SET
1878+
latest_event_id = excluded.latest_event_id,
1879+
topological_ordering = excluded.topological_ordering,
1880+
stream_ordering = excluded.stream_ordering
1881+
WHERE
1882+
threads.topological_ordering <= excluded.topological_ordering AND
1883+
threads.stream_ordering < excluded.stream_ordering
1884+
"""
1885+
1886+
txn.execute(
1887+
sql,
1888+
(
1889+
event.room_id,
1890+
relation.parent_id,
1891+
event.event_id,
1892+
event.depth,
1893+
event.internal_metadata.stream_ordering,
1894+
),
1895+
)
1896+
18691897
def _handle_insertion_event(
18701898
self, txn: LoggingTransaction, event: EventBase
18711899
) -> None:
@@ -1989,13 +2017,14 @@ def _handle_batch_event(self, txn: LoggingTransaction, event: EventBase) -> None
19892017
txn.execute(sql, (batch_id,))
19902018

19912019
def _handle_redact_relations(
1992-
self, txn: LoggingTransaction, redacted_event_id: str
2020+
self, txn: LoggingTransaction, room_id: str, redacted_event_id: str
19932021
) -> None:
19942022
"""Handles receiving a redaction and checking whether the redacted event
19952023
has any relations which must be removed from the database.
19962024
19972025
Args:
19982026
txn
2027+
room_id: The room ID of the event that was redacted.
19992028
redacted_event_id: The event that was redacted.
20002029
"""
20012030

@@ -2024,6 +2053,9 @@ def _handle_redact_relations(
20242053
self.store._invalidate_cache_and_stream(
20252054
txn, self.store.get_thread_participated, (redacted_relates_to,)
20262055
)
2056+
self.store._invalidate_cache_and_stream(
2057+
txn, self.store.get_threads, (room_id,)
2058+
)
20272059

20282060
self.db_pool.simple_delete_txn(
20292061
txn, table="event_relations", keyvalues={"event_id": redacted_event_id}

0 commit comments

Comments
 (0)