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

Commit

Permalink
Add query parameter ts to allow appservices set the `origin_server_…
Browse files Browse the repository at this point in the history
…ts` for state events. (#11866)

MSC3316 declares that both /rooms/{roomId}/send and /rooms/{roomId}/state
should accept a ts parameter for appservices. This change expands support
to /state and adds tests.
  • Loading branch information
lukasdenk committed Oct 3, 2022
1 parent a423f45 commit 719488d
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 15 deletions.
1 change: 1 addition & 0 deletions changelog.d/11866.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow application services to set the `origin_server_ts` of a state event by providing the query parameter `ts` in `PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}`, per [MSC3316](https://github.com/matrix-org/matrix-doc/pull/3316). Contributed by @lukasdenk.
13 changes: 13 additions & 0 deletions synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ async def _local_membership_update(
require_consent: bool = True,
outlier: bool = False,
historical: bool = False,
origin_server_ts: Optional[int] = None,
) -> Tuple[str, int]:
"""
Internal membership update function to get an existing event or create
Expand Down Expand Up @@ -361,6 +362,8 @@ async def _local_membership_update(
historical: Indicates whether the message is being inserted
back in time around some existing events. This is used to skip
a few checks and mark the event as backfilled.
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
the current timestamp if set to None.
Returns:
Tuple of event ID and stream ordering position
Expand Down Expand Up @@ -399,6 +402,7 @@ async def _local_membership_update(
"state_key": user_id,
# For backwards compatibility:
"membership": membership,
"origin_server_ts": origin_server_ts,
},
txn_id=txn_id,
allow_no_prev_events=allow_no_prev_events,
Expand Down Expand Up @@ -504,6 +508,7 @@ async def update_membership(
prev_event_ids: Optional[List[str]] = None,
state_event_ids: Optional[List[str]] = None,
depth: Optional[int] = None,
origin_server_ts: Optional[int] = None,
) -> Tuple[str, int]:
"""Update a user's membership in a room.
Expand Down Expand Up @@ -542,6 +547,8 @@ async def update_membership(
depth: Override the depth used to order the event in the DAG.
Should normally be set to None, which will cause the depth to be calculated
based on the prev_events.
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
the current timestamp if set to None.
Returns:
A tuple of the new event ID and stream ID.
Expand Down Expand Up @@ -583,6 +590,7 @@ async def update_membership(
prev_event_ids=prev_event_ids,
state_event_ids=state_event_ids,
depth=depth,
origin_server_ts=origin_server_ts,
)

return result
Expand All @@ -606,6 +614,7 @@ async def update_membership_locked(
prev_event_ids: Optional[List[str]] = None,
state_event_ids: Optional[List[str]] = None,
depth: Optional[int] = None,
origin_server_ts: Optional[int] = None,
) -> Tuple[str, int]:
"""Helper for update_membership.
Expand Down Expand Up @@ -646,6 +655,8 @@ async def update_membership_locked(
depth: Override the depth used to order the event in the DAG.
Should normally be set to None, which will cause the depth to be calculated
based on the prev_events.
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
the current timestamp if set to None.
Returns:
A tuple of the new event ID and stream ID.
Expand Down Expand Up @@ -785,6 +796,7 @@ async def update_membership_locked(
require_consent=require_consent,
outlier=outlier,
historical=historical,
origin_server_ts=origin_server_ts,
)

latest_event_ids = await self.store.get_prev_events_for_room(room_id)
Expand Down Expand Up @@ -1030,6 +1042,7 @@ async def update_membership_locked(
content=content,
require_consent=require_consent,
outlier=outlier,
origin_server_ts=origin_server_ts,
)

async def _should_perform_remote_join(
Expand Down
34 changes: 21 additions & 13 deletions synapse/rest/client/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,15 +268,9 @@ async def on_PUT(

content = parse_json_object_from_request(request)

event_dict = {
"type": event_type,
"content": content,
"room_id": room_id,
"sender": requester.user.to_string(),
}

if state_key is not None:
event_dict["state_key"] = state_key
origin_server_ts = None
if requester.app_service:
origin_server_ts = parse_integer(request, "ts")

try:
if event_type == EventTypes.Member:
Expand All @@ -287,8 +281,22 @@ async def on_PUT(
room_id=room_id,
action=membership,
content=content,
origin_server_ts=origin_server_ts,
)
else:
event_dict: JsonDict = {
"type": event_type,
"content": content,
"room_id": room_id,
"sender": requester.user.to_string(),
}

if state_key is not None:
event_dict["state_key"] = state_key

if origin_server_ts is not None:
event_dict["origin_server_ts"] = origin_server_ts

(
event,
_,
Expand Down Expand Up @@ -333,10 +341,10 @@ async def on_POST(
"sender": requester.user.to_string(),
}

# Twisted will have processed the args by now.
assert request.args is not None
if b"ts" in request.args and requester.app_service:
event_dict["origin_server_ts"] = parse_integer(request, "ts", 0)
if requester.app_service:
origin_server_ts = parse_integer(request, "ts")
if origin_server_ts is not None:
event_dict["origin_server_ts"] = origin_server_ts

try:
(
Expand Down
119 changes: 117 additions & 2 deletions tests/rest/client/test_rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import json
from http import HTTPStatus
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
from unittest.mock import Mock, call
from unittest.mock import Mock, call, patch
from urllib import parse as urlparse

from parameterized import param, parameterized
Expand All @@ -39,9 +39,10 @@
RoomTypes,
)
from synapse.api.errors import Codes, HttpResponseException
from synapse.appservice import ApplicationService
from synapse.handlers.pagination import PurgeStatus
from synapse.rest import admin
from synapse.rest.client import account, directory, login, profile, room, sync
from synapse.rest.client import account, directory, login, profile, register, room, sync
from synapse.server import HomeServer
from synapse.types import JsonDict, RoomAlias, UserID, create_requester
from synapse.util import Clock
Expand Down Expand Up @@ -1252,6 +1253,120 @@ async def user_may_join_room(
)


class RoomAppserviceTsParamTestCase(unittest.HomeserverTestCase):
servlets = [
room.register_servlets,
synapse.rest.admin.register_servlets,
register.register_servlets,
]

def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.appservice_user, _ = self.register_appservice_user(
"as_user_potato", self.appservice.token
)

# Create a room as the appservice user.
args = {
"access_token": self.appservice.token,
"user_id": self.appservice_user,
}
channel = self.make_request(
"POST",
f"/_matrix/client/r0/createRoom?{urlparse.urlencode(args)}",
content={"visibility": "public"},
)

assert channel.code == 200
self.room = channel.json_body["room_id"]

self.main_store = self.hs.get_datastores().main

def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
config = self.default_config()

self.appservice = ApplicationService(
token="i_am_an_app_service",
id="1234",
namespaces={"users": [{"regex": r"@as_user.*", "exclusive": True}]},
# Note: this user does not have to match the regex above
sender="@as_main:test",
)

mock_load_appservices = Mock(return_value=[self.appservice])
with patch(
"synapse.storage.databases.main.appservice.load_appservices",
mock_load_appservices,
):
hs = self.setup_test_homeserver(config=config)
return hs

def test_send_event_ts(self) -> None:
"""Test sending a non-state event with a custom timestamp."""
ts = 1

url_params = {
"user_id": self.appservice_user,
"ts": ts,
}
channel = self.make_request(
"PUT",
path=f"/_matrix/client/r0/rooms/{self.room}/send/m.room.message/1234?"
+ urlparse.urlencode(url_params),
content={"body": "test", "msgtype": "m.text"},
access_token=self.appservice.token,
)
self.assertEqual(channel.code, 200, channel.json_body)
event_id = channel.json_body["event_id"]

# Ensure the event was persisted with the correct timestamp.
res = self.get_success(self.main_store.get_event(event_id))
self.assertEquals(ts, res.origin_server_ts)

def test_send_state_event_ts(self) -> None:
"""Test sending a state event with a custom timestamp."""
ts = 1

url_params = {
"user_id": self.appservice_user,
"ts": ts,
}
channel = self.make_request(
"PUT",
path=f"/_matrix/client/r0/rooms/{self.room}/state/m.room.name?"
+ urlparse.urlencode(url_params),
content={"name": "test"},
access_token=self.appservice.token,
)
self.assertEqual(channel.code, 200, channel.json_body)
event_id = channel.json_body["event_id"]

# Ensure the event was persisted with the correct timestamp.
res = self.get_success(self.main_store.get_event(event_id))
self.assertEquals(ts, res.origin_server_ts)

def test_send_membership_event_ts(self) -> None:
"""Test sending a membership event with a custom timestamp."""
ts = 1

url_params = {
"user_id": self.appservice_user,
"ts": ts,
}
channel = self.make_request(
"PUT",
path=f"/_matrix/client/r0/rooms/{self.room}/state/m.room.member/{self.appservice_user}?"
+ urlparse.urlencode(url_params),
content={"membership": "join", "display_name": "test"},
access_token=self.appservice.token,
)
self.assertEqual(channel.code, 200, channel.json_body)
event_id = channel.json_body["event_id"]

# Ensure the event was persisted with the correct timestamp.
res = self.get_success(self.main_store.get_event(event_id))
self.assertEquals(ts, res.origin_server_ts)


class RoomJoinRatelimitTestCase(RoomBase):
user_id = "@sid1:red"

Expand Down

0 comments on commit 719488d

Please sign in to comment.