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

Commit a6f1a3a

Browse files
Add MSC3030 experimental client and federation API endpoints to get the closest event to a given timestamp (#9445)
MSC3030: matrix-org/matrix-spec-proposals#3030 Client API endpoint. This will also go and fetch from the federation API endpoint if unable to find an event locally or we found an extremity with possibly a closer event we don't know about. ``` GET /_matrix/client/unstable/org.matrix.msc3030/rooms/<roomID>/timestamp_to_event?ts=<timestamp>&dir=<direction> { "event_id": ... "origin_server_ts": ... } ``` Federation API endpoint: ``` GET /_matrix/federation/unstable/org.matrix.msc3030/timestamp_to_event/<roomID>?ts=<timestamp>&dir=<direction> { "event_id": ... "origin_server_ts": ... } ``` Co-authored-by: Erik Johnston <erik@matrix.org>
1 parent 84dc50e commit a6f1a3a

File tree

13 files changed

+674
-31
lines changed

13 files changed

+674
-31
lines changed

changelog.d/9445.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add [MSC3030](https://github.com/matrix-org/matrix-doc/pull/3030) experimental client and federation API endpoints to get the closest event to a given timestamp.

synapse/config/experimental.py

+3
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@ def read_config(self, config: JsonDict, **kwargs):
4646

4747
# MSC3266 (room summary api)
4848
self.msc3266_enabled: bool = experimental.get("msc3266_enabled", False)
49+
50+
# MSC3030 (Jump to date API endpoint)
51+
self.msc3030_enabled: bool = experimental.get("msc3030_enabled", False)

synapse/federation/federation_client.py

+77
Original file line numberDiff line numberDiff line change
@@ -1517,6 +1517,83 @@ async def send_request(
15171517
self._get_room_hierarchy_cache[(room_id, suggested_only)] = result
15181518
return result
15191519

1520+
async def timestamp_to_event(
1521+
self, destination: str, room_id: str, timestamp: int, direction: str
1522+
) -> "TimestampToEventResponse":
1523+
"""
1524+
Calls a remote federating server at `destination` asking for their
1525+
closest event to the given timestamp in the given direction. Also
1526+
validates the response to always return the expected keys or raises an
1527+
error.
1528+
1529+
Args:
1530+
destination: Domain name of the remote homeserver
1531+
room_id: Room to fetch the event from
1532+
timestamp: The point in time (inclusive) we should navigate from in
1533+
the given direction to find the closest event.
1534+
direction: ["f"|"b"] to indicate whether we should navigate forward
1535+
or backward from the given timestamp to find the closest event.
1536+
1537+
Returns:
1538+
A parsed TimestampToEventResponse including the closest event_id
1539+
and origin_server_ts
1540+
1541+
Raises:
1542+
Various exceptions when the request fails
1543+
InvalidResponseError when the response does not have the correct
1544+
keys or wrong types
1545+
"""
1546+
remote_response = await self.transport_layer.timestamp_to_event(
1547+
destination, room_id, timestamp, direction
1548+
)
1549+
1550+
if not isinstance(remote_response, dict):
1551+
raise InvalidResponseError(
1552+
"Response must be a JSON dictionary but received %r" % remote_response
1553+
)
1554+
1555+
try:
1556+
return TimestampToEventResponse.from_json_dict(remote_response)
1557+
except ValueError as e:
1558+
raise InvalidResponseError(str(e))
1559+
1560+
1561+
@attr.s(frozen=True, slots=True, auto_attribs=True)
1562+
class TimestampToEventResponse:
1563+
"""Typed response dictionary for the federation /timestamp_to_event endpoint"""
1564+
1565+
event_id: str
1566+
origin_server_ts: int
1567+
1568+
# the raw data, including the above keys
1569+
data: JsonDict
1570+
1571+
@classmethod
1572+
def from_json_dict(cls, d: JsonDict) -> "TimestampToEventResponse":
1573+
"""Parsed response from the federation /timestamp_to_event endpoint
1574+
1575+
Args:
1576+
d: JSON object response to be parsed
1577+
1578+
Raises:
1579+
ValueError if d does not the correct keys or they are the wrong types
1580+
"""
1581+
1582+
event_id = d.get("event_id")
1583+
if not isinstance(event_id, str):
1584+
raise ValueError(
1585+
"Invalid response: 'event_id' must be a str but received %r" % event_id
1586+
)
1587+
1588+
origin_server_ts = d.get("origin_server_ts")
1589+
if not isinstance(origin_server_ts, int):
1590+
raise ValueError(
1591+
"Invalid response: 'origin_server_ts' must be a int but received %r"
1592+
% origin_server_ts
1593+
)
1594+
1595+
return cls(event_id, origin_server_ts, d)
1596+
15201597

15211598
@attr.s(frozen=True, slots=True, auto_attribs=True)
15221599
class FederationSpaceSummaryEventResult:

synapse/federation/federation_server.py

+43
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def __init__(self, hs: "HomeServer"):
110110
super().__init__(hs)
111111

112112
self.handler = hs.get_federation_handler()
113+
self.storage = hs.get_storage()
113114
self._federation_event_handler = hs.get_federation_event_handler()
114115
self.state = hs.get_state_handler()
115116
self._event_auth_handler = hs.get_event_auth_handler()
@@ -200,6 +201,48 @@ async def on_backfill_request(
200201

201202
return 200, res
202203

204+
async def on_timestamp_to_event_request(
205+
self, origin: str, room_id: str, timestamp: int, direction: str
206+
) -> Tuple[int, Dict[str, Any]]:
207+
"""When we receive a federated `/timestamp_to_event` request,
208+
handle all of the logic for validating and fetching the event.
209+
210+
Args:
211+
origin: The server we received the event from
212+
room_id: Room to fetch the event from
213+
timestamp: The point in time (inclusive) we should navigate from in
214+
the given direction to find the closest event.
215+
direction: ["f"|"b"] to indicate whether we should navigate forward
216+
or backward from the given timestamp to find the closest event.
217+
218+
Returns:
219+
Tuple indicating the response status code and dictionary response
220+
body including `event_id`.
221+
"""
222+
with (await self._server_linearizer.queue((origin, room_id))):
223+
origin_host, _ = parse_server_name(origin)
224+
await self.check_server_matches_acl(origin_host, room_id)
225+
226+
# We only try to fetch data from the local database
227+
event_id = await self.store.get_event_id_for_timestamp(
228+
room_id, timestamp, direction
229+
)
230+
if event_id:
231+
event = await self.store.get_event(
232+
event_id, allow_none=False, allow_rejected=False
233+
)
234+
235+
return 200, {
236+
"event_id": event_id,
237+
"origin_server_ts": event.origin_server_ts,
238+
}
239+
240+
raise SynapseError(
241+
404,
242+
"Unable to find event from %s in direction %s" % (timestamp, direction),
243+
errcode=Codes.NOT_FOUND,
244+
)
245+
203246
async def on_incoming_transaction(
204247
self,
205248
origin: str,

synapse/federation/transport/client.py

+36
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,42 @@ async def backfill(
148148
destination, path=path, args=args, try_trailing_slash_on_400=True
149149
)
150150

151+
@log_function
152+
async def timestamp_to_event(
153+
self, destination: str, room_id: str, timestamp: int, direction: str
154+
) -> Union[JsonDict, List]:
155+
"""
156+
Calls a remote federating server at `destination` asking for their
157+
closest event to the given timestamp in the given direction.
158+
159+
Args:
160+
destination: Domain name of the remote homeserver
161+
room_id: Room to fetch the event from
162+
timestamp: The point in time (inclusive) we should navigate from in
163+
the given direction to find the closest event.
164+
direction: ["f"|"b"] to indicate whether we should navigate forward
165+
or backward from the given timestamp to find the closest event.
166+
167+
Returns:
168+
Response dict received from the remote homeserver.
169+
170+
Raises:
171+
Various exceptions when the request fails
172+
"""
173+
path = _create_path(
174+
FEDERATION_UNSTABLE_PREFIX,
175+
"/org.matrix.msc3030/timestamp_to_event/%s",
176+
room_id,
177+
)
178+
179+
args = {"ts": [str(timestamp)], "dir": [direction]}
180+
181+
remote_response = await self.client.get_json(
182+
destination, path=path, args=args, try_trailing_slash_on_400=True
183+
)
184+
185+
return remote_response
186+
151187
@log_function
152188
async def send_transaction(
153189
self,

synapse/federation/transport/server/__init__.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
Authenticator,
2323
BaseFederationServlet,
2424
)
25-
from synapse.federation.transport.server.federation import FEDERATION_SERVLET_CLASSES
25+
from synapse.federation.transport.server.federation import (
26+
FEDERATION_SERVLET_CLASSES,
27+
FederationTimestampLookupServlet,
28+
)
2629
from synapse.federation.transport.server.groups_local import GROUP_LOCAL_SERVLET_CLASSES
2730
from synapse.federation.transport.server.groups_server import (
2831
GROUP_SERVER_SERVLET_CLASSES,
@@ -324,6 +327,13 @@ def register_servlets(
324327
)
325328

326329
for servletclass in DEFAULT_SERVLET_GROUPS[servlet_group]:
330+
# Only allow the `/timestamp_to_event` servlet if msc3030 is enabled
331+
if (
332+
servletclass == FederationTimestampLookupServlet
333+
and not hs.config.experimental.msc3030_enabled
334+
):
335+
continue
336+
327337
servletclass(
328338
hs=hs,
329339
authenticator=authenticator,

synapse/federation/transport/server/federation.py

+41
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,46 @@ async def on_GET(
174174
return await self.handler.on_backfill_request(origin, room_id, versions, limit)
175175

176176

177+
class FederationTimestampLookupServlet(BaseFederationServerServlet):
178+
"""
179+
API endpoint to fetch the `event_id` of the closest event to the given
180+
timestamp (`ts` query parameter) in the given direction (`dir` query
181+
parameter).
182+
183+
Useful for other homeservers when they're unable to find an event locally.
184+
185+
`ts` is a timestamp in milliseconds where we will find the closest event in
186+
the given direction.
187+
188+
`dir` can be `f` or `b` to indicate forwards and backwards in time from the
189+
given timestamp.
190+
191+
GET /_matrix/federation/unstable/org.matrix.msc3030/timestamp_to_event/<roomID>?ts=<timestamp>&dir=<direction>
192+
{
193+
"event_id": ...
194+
}
195+
"""
196+
197+
PATH = "/timestamp_to_event/(?P<room_id>[^/]*)/?"
198+
PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc3030"
199+
200+
async def on_GET(
201+
self,
202+
origin: str,
203+
content: Literal[None],
204+
query: Dict[bytes, List[bytes]],
205+
room_id: str,
206+
) -> Tuple[int, JsonDict]:
207+
timestamp = parse_integer_from_args(query, "ts", required=True)
208+
direction = parse_string_from_args(
209+
query, "dir", default="f", allowed_values=["f", "b"], required=True
210+
)
211+
212+
return await self.handler.on_timestamp_to_event_request(
213+
origin, room_id, timestamp, direction
214+
)
215+
216+
177217
class FederationQueryServlet(BaseFederationServerServlet):
178218
PATH = "/query/(?P<query_type>[^/]*)"
179219

@@ -683,6 +723,7 @@ async def on_GET(
683723
FederationStateV1Servlet,
684724
FederationStateIdsServlet,
685725
FederationBackfillServlet,
726+
FederationTimestampLookupServlet,
686727
FederationQueryServlet,
687728
FederationMakeJoinServlet,
688729
FederationMakeLeaveServlet,

synapse/handlers/federation.py

+31-30
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,37 @@
6868
logger = logging.getLogger(__name__)
6969

7070

71+
def get_domains_from_state(state: StateMap[EventBase]) -> List[Tuple[str, int]]:
72+
"""Get joined domains from state
73+
74+
Args:
75+
state: State map from type/state key to event.
76+
77+
Returns:
78+
Returns a list of servers with the lowest depth of their joins.
79+
Sorted by lowest depth first.
80+
"""
81+
joined_users = [
82+
(state_key, int(event.depth))
83+
for (e_type, state_key), event in state.items()
84+
if e_type == EventTypes.Member and event.membership == Membership.JOIN
85+
]
86+
87+
joined_domains: Dict[str, int] = {}
88+
for u, d in joined_users:
89+
try:
90+
dom = get_domain_from_id(u)
91+
old_d = joined_domains.get(dom)
92+
if old_d:
93+
joined_domains[dom] = min(d, old_d)
94+
else:
95+
joined_domains[dom] = d
96+
except Exception:
97+
pass
98+
99+
return sorted(joined_domains.items(), key=lambda d: d[1])
100+
101+
71102
class FederationHandler:
72103
"""Handles general incoming federation requests
73104
@@ -268,36 +299,6 @@ async def _maybe_backfill_inner(
268299

269300
curr_state = await self.state_handler.get_current_state(room_id)
270301

271-
def get_domains_from_state(state: StateMap[EventBase]) -> List[Tuple[str, int]]:
272-
"""Get joined domains from state
273-
274-
Args:
275-
state: State map from type/state key to event.
276-
277-
Returns:
278-
Returns a list of servers with the lowest depth of their joins.
279-
Sorted by lowest depth first.
280-
"""
281-
joined_users = [
282-
(state_key, int(event.depth))
283-
for (e_type, state_key), event in state.items()
284-
if e_type == EventTypes.Member and event.membership == Membership.JOIN
285-
]
286-
287-
joined_domains: Dict[str, int] = {}
288-
for u, d in joined_users:
289-
try:
290-
dom = get_domain_from_id(u)
291-
old_d = joined_domains.get(dom)
292-
if old_d:
293-
joined_domains[dom] = min(d, old_d)
294-
else:
295-
joined_domains[dom] = d
296-
except Exception:
297-
pass
298-
299-
return sorted(joined_domains.items(), key=lambda d: d[1])
300-
301302
curr_domains = get_domains_from_state(curr_state)
302303

303304
likely_domains = [

0 commit comments

Comments
 (0)