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

Add endpoint to get an event at a given timestamp - MSC3030 #9445

Merged
merged 55 commits into from Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from 53 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
91b1b36
Add endpoint to get an event at a given timestamp
erikjohnston Feb 19, 2021
8e5fa11
Update to latest REST servlet standards
MadLittleMods Jul 15, 2021
96c48ba
Merge branch 'develop' into eric/jump-to-date
MadLittleMods Jul 26, 2021
668aa4e
Remove thread_id
MadLittleMods Jul 27, 2021
5b07487
Use updated method name check_joined_room -> check_user_in_room
MadLittleMods Jul 27, 2021
f721899
Use correct requester to sender access method
MadLittleMods Jul 27, 2021
af085ab
Use up-to-date db interaction method
MadLittleMods Jul 27, 2021
065273b
Add Complement MSC feature flag
MadLittleMods Jul 28, 2021
0e0ddda
Merge branch 'develop' into eric/jump-to-date
MadLittleMods Nov 10, 2021
e321ef7
Fix query sort order so it returns the closest event before/after
MadLittleMods Nov 12, 2021
e21e4b5
Add ?dir parameter
MadLittleMods Nov 12, 2021
fa15989
Add federated /timestamp_to_event endpoint and logic to ask
MadLittleMods Nov 17, 2021
ec2695d
Determine forward extremity by edges
MadLittleMods Nov 17, 2021
22a93c3
Add experimental feature flag for MSC3030
MadLittleMods Nov 17, 2021
b311853
Document and move to unstable prefixes
MadLittleMods Nov 17, 2021
5638123
Add changelog
MadLittleMods Nov 17, 2021
612b51f
Fix lints
MadLittleMods Nov 17, 2021
654d7ae
Fix lint
MadLittleMods Nov 17, 2021
6280d36
Fix lint
MadLittleMods Nov 17, 2021
8766b0a
Filter events before handing them over federation
MadLittleMods Nov 25, 2021
86a2642
Don't spam logs for 404's
MadLittleMods Nov 25, 2021
5a2c997
Allow returning event_ids for hidden events according to history visi…
MadLittleMods Nov 25, 2021
bc3ba38
Ignore rejected events and look for gaps instead
MadLittleMods Nov 25, 2021
edac953
We only return event or raise an exception
MadLittleMods Nov 25, 2021
ab800e3
Only return ValueError from storage layer
MadLittleMods Nov 25, 2021
9800a4b
Add docstring
MadLittleMods Nov 25, 2021
8523bf3
Better description for event_next_to_gap
MadLittleMods Nov 25, 2021
f05c292
Fix lints
MadLittleMods Nov 25, 2021
984a14b
Merge branch 'develop' into eric/jump-to-date
MadLittleMods Nov 25, 2021
dae7e0a
Remove filter_events_for_server because always ok to see event_id
MadLittleMods Nov 29, 2021
87ac1ed
Rename to is_event_next_to_gap
MadLittleMods Nov 29, 2021
2a5b622
Remove extra space typo
MadLittleMods Nov 29, 2021
0610fac
Include inclusive comment on all comment docs
MadLittleMods Nov 29, 2021
183e1bf
Remove redundant continues
MadLittleMods Nov 29, 2021
5362bd3
Fix query rejections and edge cases
MadLittleMods Nov 30, 2021
76ac526
Update function name to portray what it accepts, event_id
MadLittleMods Nov 30, 2021
58d67f2
Fix backward gap detection
MadLittleMods Nov 30, 2021
63d61fc
Type the transport client for /timestamp_to_event requests
MadLittleMods Nov 30, 2021
70420e5
Optimize when we ask other federated homeservers
MadLittleMods Nov 30, 2021
a8644b9
Only return remote event when closer
MadLittleMods Nov 30, 2021
c38984c
Add origin_server_ts to client endpoint
MadLittleMods Nov 30, 2021
ed1360a
Make asserts more obvious
MadLittleMods Nov 30, 2021
13371a6
Better comments
MadLittleMods Nov 30, 2021
d137292
Also check for the event itself as a backward extremity
MadLittleMods Nov 30, 2021
662366a
Remove redundant NPE check
MadLittleMods Nov 30, 2021
dd7e689
Make more comment clear on why not and what we can use it to rely on
MadLittleMods Nov 30, 2021
e7d2120
Fix typo
MadLittleMods Nov 30, 2021
5660fde
Use federation_client and better validation
MadLittleMods Nov 30, 2021
c3c404b
Louder warnings about real problems
MadLittleMods Dec 1, 2021
1ff2db4
Optimize query to a single IN clause
MadLittleMods Dec 1, 2021
067d67a
Merge branch 'develop' into eric/jump-to-date
MadLittleMods Dec 1, 2021
5888eba
Fix lint
MadLittleMods Dec 1, 2021
68704f4
Fix mypy lints
MadLittleMods Dec 1, 2021
3ee5d0c
Use %r to protect from ascii junk
MadLittleMods Dec 2, 2021
2621e5d
Remove MSC3030 complement test flag because unknown state of tests at…
MadLittleMods Dec 2, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/9445.feature
@@ -0,0 +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.
2 changes: 1 addition & 1 deletion scripts-dev/complement.sh
Expand Up @@ -65,4 +65,4 @@ if [[ -n "$1" ]]; then
fi

# Run the tests!
go test -v -tags synapse_blacklist,msc2403 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests/...
go test -v -tags synapse_blacklist,msc2403,msc3030 -count=1 $EXTRA_COMPLEMENT_ARGS ./tests/...
3 changes: 3 additions & 0 deletions synapse/config/experimental.py
Expand Up @@ -46,3 +46,6 @@ def read_config(self, config: JsonDict, **kwargs):

# MSC3266 (room summary api)
self.msc3266_enabled: bool = experimental.get("msc3266_enabled", False)

# MSC3030 (Jump to date API endpoint)
self.msc3030_enabled: bool = experimental.get("msc3030_enabled", False)
77 changes: 77 additions & 0 deletions synapse/federation/federation_client.py
Expand Up @@ -1517,6 +1517,83 @@ async def send_request(
self._get_room_hierarchy_cache[(room_id, suggested_only)] = result
return result

async def timestamp_to_event(
self, destination: str, room_id: str, timestamp: int, direction: str
) -> "TimestampToEventResponse":
"""
Calls a remote federating server at `destination` asking for their
closest event to the given timestamp in the given direction. Also
validates the response to always return the expected keys or raises an
error.

Args:
destination: Domain name of the remote homeserver
room_id: Room to fetch the event from
timestamp: The point in time (inclusive) we should navigate from in
the given direction to find the closest event.
direction: ["f"|"b"] to indicate whether we should navigate forward
or backward from the given timestamp to find the closest event.

Returns:
A parsed TimestampToEventResponse including the closest event_id
and origin_server_ts

Raises:
Various exceptions when the request fails
InvalidResponseError when the response does not have the correct
keys or wrong types
"""
remote_response = await self.transport_layer.timestamp_to_event(
destination, room_id, timestamp, direction
)

if not isinstance(remote_response, dict):
raise InvalidResponseError(
"Response must be a JSON dictionary but received %s" % remote_response
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
)

try:
return TimestampToEventResponse.from_json_dict(remote_response)
except ValueError as e:
raise InvalidResponseError(str(e))


@attr.s(frozen=True, slots=True, auto_attribs=True)
class TimestampToEventResponse:
"""Typed response dictionary for the federation /timestamp_to_event endpoint"""

event_id: str
origin_server_ts: int

# the raw data, including the above keys
data: JsonDict

@classmethod
def from_json_dict(cls, d: JsonDict) -> "TimestampToEventResponse":
"""Parsed response from the federation /timestamp_to_event endpoint

Args:
d: JSON object response to be parsed

Raises:
ValueError if d does not the correct keys or they are the wrong types
"""

event_id = d.get("event_id")
if not isinstance(event_id, str):
raise ValueError(
"Invalid response: 'event_id' must be a str but received %s" % event_id
)

origin_server_ts = d.get("origin_server_ts")
if not isinstance(origin_server_ts, int):
raise ValueError(
"Invalid response: 'origin_server_ts' must be a int but received %s"
% origin_server_ts
)

return cls(event_id, origin_server_ts, d)


@attr.s(frozen=True, slots=True, auto_attribs=True)
class FederationSpaceSummaryEventResult:
Expand Down
43 changes: 43 additions & 0 deletions synapse/federation/federation_server.py
Expand Up @@ -110,6 +110,7 @@ def __init__(self, hs: "HomeServer"):
super().__init__(hs)

self.handler = hs.get_federation_handler()
self.storage = hs.get_storage()
self._federation_event_handler = hs.get_federation_event_handler()
self.state = hs.get_state_handler()
self._event_auth_handler = hs.get_event_auth_handler()
Expand Down Expand Up @@ -200,6 +201,48 @@ async def on_backfill_request(

return 200, res

async def on_timestamp_to_event_request(
self, origin: str, room_id: str, timestamp: int, direction: str
) -> Tuple[int, Dict[str, Any]]:
"""When we receive a federated `/timestamp_to_event` request,
handle all of the logic for validating and fetching the event.

Args:
origin: The server we received the event from
room_id: Room to fetch the event from
timestamp: The point in time (inclusive) we should navigate from in
the given direction to find the closest event.
direction: ["f"|"b"] to indicate whether we should navigate forward
or backward from the given timestamp to find the closest event.

Returns:
Tuple indicating the response status code and dictionary response
body including `event_id`.
"""
with (await self._server_linearizer.queue((origin, room_id))):
origin_host, _ = parse_server_name(origin)
await self.check_server_matches_acl(origin_host, room_id)

# We only try to fetch data from the local database
event_id = await self.store.get_event_id_for_timestamp(
room_id, timestamp, direction
)
MadLittleMods marked this conversation as resolved.
Show resolved Hide resolved
if event_id:
event = await self.store.get_event(
event_id, allow_none=False, allow_rejected=False
)

return 200, {
"event_id": event_id,
"origin_server_ts": event.origin_server_ts,
}

raise SynapseError(
404,
"Unable to find event from %s in direction %s" % (timestamp, direction),
errcode=Codes.NOT_FOUND,
)

async def on_incoming_transaction(
self,
origin: str,
Expand Down
36 changes: 36 additions & 0 deletions synapse/federation/transport/client.py
Expand Up @@ -148,6 +148,42 @@ async def backfill(
destination, path=path, args=args, try_trailing_slash_on_400=True
)

@log_function
async def timestamp_to_event(
self, destination: str, room_id: str, timestamp: int, direction: str
) -> Union[JsonDict, List]:
"""
Calls a remote federating server at `destination` asking for their
closest event to the given timestamp in the given direction.

Args:
destination: Domain name of the remote homeserver
room_id: Room to fetch the event from
timestamp: The point in time (inclusive) we should navigate from in
the given direction to find the closest event.
direction: ["f"|"b"] to indicate whether we should navigate forward
or backward from the given timestamp to find the closest event.

Returns:
Response dict received from the remote homeserver.

Raises:
Various exceptions when the request fails
"""
path = _create_path(
FEDERATION_UNSTABLE_PREFIX,
"/org.matrix.msc3030/timestamp_to_event/%s",
room_id,
)

args = {"ts": [str(timestamp)], "dir": [direction]}

remote_response = await self.client.get_json(
destination, path=path, args=args, try_trailing_slash_on_400=True
)

return remote_response

@log_function
async def send_transaction(
self,
Expand Down
12 changes: 11 additions & 1 deletion synapse/federation/transport/server/__init__.py
Expand Up @@ -22,7 +22,10 @@
Authenticator,
BaseFederationServlet,
)
from synapse.federation.transport.server.federation import FEDERATION_SERVLET_CLASSES
from synapse.federation.transport.server.federation import (
FEDERATION_SERVLET_CLASSES,
FederationTimestampLookupServlet,
)
from synapse.federation.transport.server.groups_local import GROUP_LOCAL_SERVLET_CLASSES
from synapse.federation.transport.server.groups_server import (
GROUP_SERVER_SERVLET_CLASSES,
Expand Down Expand Up @@ -324,6 +327,13 @@ def register_servlets(
)

for servletclass in DEFAULT_SERVLET_GROUPS[servlet_group]:
# Only allow the `/timestamp_to_event` servlet if msc3030 is enabled
if (
servletclass == FederationTimestampLookupServlet
and not hs.config.experimental.msc3030_enabled
):
continue
squahtx marked this conversation as resolved.
Show resolved Hide resolved

servletclass(
hs=hs,
authenticator=authenticator,
Expand Down
41 changes: 41 additions & 0 deletions synapse/federation/transport/server/federation.py
Expand Up @@ -174,6 +174,46 @@ async def on_GET(
return await self.handler.on_backfill_request(origin, room_id, versions, limit)


class FederationTimestampLookupServlet(BaseFederationServerServlet):
"""
API endpoint to fetch the `event_id` of the closest event to the given
timestamp (`ts` query parameter) in the given direction (`dir` query
parameter).

Useful for other homeservers when they're unable to find an event locally.

`ts` is a timestamp in milliseconds where we will find the closest event in
the given direction.

`dir` can be `f` or `b` to indicate forwards and backwards in time from the
given timestamp.

GET /_matrix/federation/unstable/org.matrix.msc3030/timestamp_to_event/<roomID>?ts=<timestamp>&dir=<direction>
{
"event_id": ...
}
"""

PATH = "/timestamp_to_event/(?P<room_id>[^/]*)/?"
PREFIX = FEDERATION_UNSTABLE_PREFIX + "/org.matrix.msc3030"

async def on_GET(
self,
origin: str,
content: Literal[None],
query: Dict[bytes, List[bytes]],
room_id: str,
) -> Tuple[int, JsonDict]:
timestamp = parse_integer_from_args(query, "ts", required=True)
direction = parse_string_from_args(
query, "dir", default="f", allowed_values=["f", "b"], required=True
)

return await self.handler.on_timestamp_to_event_request(
origin, room_id, timestamp, direction
)


class FederationQueryServlet(BaseFederationServerServlet):
PATH = "/query/(?P<query_type>[^/]*)"

Expand Down Expand Up @@ -683,6 +723,7 @@ async def on_GET(
FederationStateV1Servlet,
FederationStateIdsServlet,
FederationBackfillServlet,
FederationTimestampLookupServlet,
FederationQueryServlet,
FederationMakeJoinServlet,
FederationMakeLeaveServlet,
Expand Down
61 changes: 31 additions & 30 deletions synapse/handlers/federation.py
Expand Up @@ -68,6 +68,37 @@
logger = logging.getLogger(__name__)


def get_domains_from_state(state: StateMap[EventBase]) -> List[Tuple[str, int]]:
"""Get joined domains from state

Args:
state: State map from type/state key to event.

Returns:
Returns a list of servers with the lowest depth of their joins.
Sorted by lowest depth first.
"""
joined_users = [
(state_key, int(event.depth))
for (e_type, state_key), event in state.items()
if e_type == EventTypes.Member and event.membership == Membership.JOIN
]

joined_domains: Dict[str, int] = {}
for u, d in joined_users:
try:
dom = get_domain_from_id(u)
old_d = joined_domains.get(dom)
if old_d:
joined_domains[dom] = min(d, old_d)
else:
joined_domains[dom] = d
except Exception:
pass

return sorted(joined_domains.items(), key=lambda d: d[1])


class FederationHandler:
"""Handles general incoming federation requests

Expand Down Expand Up @@ -268,36 +299,6 @@ async def _maybe_backfill_inner(

curr_state = await self.state_handler.get_current_state(room_id)

def get_domains_from_state(state: StateMap[EventBase]) -> List[Tuple[str, int]]:
"""Get joined domains from state

Args:
state: State map from type/state key to event.

Returns:
Returns a list of servers with the lowest depth of their joins.
Sorted by lowest depth first.
"""
joined_users = [
(state_key, int(event.depth))
for (e_type, state_key), event in state.items()
if e_type == EventTypes.Member and event.membership == Membership.JOIN
]

joined_domains: Dict[str, int] = {}
for u, d in joined_users:
try:
dom = get_domain_from_id(u)
old_d = joined_domains.get(dom)
if old_d:
joined_domains[dom] = min(d, old_d)
else:
joined_domains[dom] = d
except Exception:
pass

return sorted(joined_domains.items(), key=lambda d: d[1])

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just de-nesting this function so we can re-use it

curr_domains = get_domains_from_state(curr_state)

likely_domains = [
Expand Down