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

Commit 719488d

Browse files
authored
Add query parameter ts to allow appservices set the origin_server_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.
1 parent a423f45 commit 719488d

File tree

4 files changed

+152
-15
lines changed

4 files changed

+152
-15
lines changed

changelog.d/11866.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

synapse/handlers/room_member.py

+13
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ async def _local_membership_update(
322322
require_consent: bool = True,
323323
outlier: bool = False,
324324
historical: bool = False,
325+
origin_server_ts: Optional[int] = None,
325326
) -> Tuple[str, int]:
326327
"""
327328
Internal membership update function to get an existing event or create
@@ -361,6 +362,8 @@ async def _local_membership_update(
361362
historical: Indicates whether the message is being inserted
362363
back in time around some existing events. This is used to skip
363364
a few checks and mark the event as backfilled.
365+
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
366+
the current timestamp if set to None.
364367
365368
Returns:
366369
Tuple of event ID and stream ordering position
@@ -399,6 +402,7 @@ async def _local_membership_update(
399402
"state_key": user_id,
400403
# For backwards compatibility:
401404
"membership": membership,
405+
"origin_server_ts": origin_server_ts,
402406
},
403407
txn_id=txn_id,
404408
allow_no_prev_events=allow_no_prev_events,
@@ -504,6 +508,7 @@ async def update_membership(
504508
prev_event_ids: Optional[List[str]] = None,
505509
state_event_ids: Optional[List[str]] = None,
506510
depth: Optional[int] = None,
511+
origin_server_ts: Optional[int] = None,
507512
) -> Tuple[str, int]:
508513
"""Update a user's membership in a room.
509514
@@ -542,6 +547,8 @@ async def update_membership(
542547
depth: Override the depth used to order the event in the DAG.
543548
Should normally be set to None, which will cause the depth to be calculated
544549
based on the prev_events.
550+
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
551+
the current timestamp if set to None.
545552
546553
Returns:
547554
A tuple of the new event ID and stream ID.
@@ -583,6 +590,7 @@ async def update_membership(
583590
prev_event_ids=prev_event_ids,
584591
state_event_ids=state_event_ids,
585592
depth=depth,
593+
origin_server_ts=origin_server_ts,
586594
)
587595

588596
return result
@@ -606,6 +614,7 @@ async def update_membership_locked(
606614
prev_event_ids: Optional[List[str]] = None,
607615
state_event_ids: Optional[List[str]] = None,
608616
depth: Optional[int] = None,
617+
origin_server_ts: Optional[int] = None,
609618
) -> Tuple[str, int]:
610619
"""Helper for update_membership.
611620
@@ -646,6 +655,8 @@ async def update_membership_locked(
646655
depth: Override the depth used to order the event in the DAG.
647656
Should normally be set to None, which will cause the depth to be calculated
648657
based on the prev_events.
658+
origin_server_ts: The origin_server_ts to use if a new event is created. Uses
659+
the current timestamp if set to None.
649660
650661
Returns:
651662
A tuple of the new event ID and stream ID.
@@ -785,6 +796,7 @@ async def update_membership_locked(
785796
require_consent=require_consent,
786797
outlier=outlier,
787798
historical=historical,
799+
origin_server_ts=origin_server_ts,
788800
)
789801

790802
latest_event_ids = await self.store.get_prev_events_for_room(room_id)
@@ -1030,6 +1042,7 @@ async def update_membership_locked(
10301042
content=content,
10311043
require_consent=require_consent,
10321044
outlier=outlier,
1045+
origin_server_ts=origin_server_ts,
10331046
)
10341047

10351048
async def _should_perform_remote_join(

synapse/rest/client/room.py

+21-13
Original file line numberDiff line numberDiff line change
@@ -268,15 +268,9 @@ async def on_PUT(
268268

269269
content = parse_json_object_from_request(request)
270270

271-
event_dict = {
272-
"type": event_type,
273-
"content": content,
274-
"room_id": room_id,
275-
"sender": requester.user.to_string(),
276-
}
277-
278-
if state_key is not None:
279-
event_dict["state_key"] = state_key
271+
origin_server_ts = None
272+
if requester.app_service:
273+
origin_server_ts = parse_integer(request, "ts")
280274

281275
try:
282276
if event_type == EventTypes.Member:
@@ -287,8 +281,22 @@ async def on_PUT(
287281
room_id=room_id,
288282
action=membership,
289283
content=content,
284+
origin_server_ts=origin_server_ts,
290285
)
291286
else:
287+
event_dict: JsonDict = {
288+
"type": event_type,
289+
"content": content,
290+
"room_id": room_id,
291+
"sender": requester.user.to_string(),
292+
}
293+
294+
if state_key is not None:
295+
event_dict["state_key"] = state_key
296+
297+
if origin_server_ts is not None:
298+
event_dict["origin_server_ts"] = origin_server_ts
299+
292300
(
293301
event,
294302
_,
@@ -333,10 +341,10 @@ async def on_POST(
333341
"sender": requester.user.to_string(),
334342
}
335343

336-
# Twisted will have processed the args by now.
337-
assert request.args is not None
338-
if b"ts" in request.args and requester.app_service:
339-
event_dict["origin_server_ts"] = parse_integer(request, "ts", 0)
344+
if requester.app_service:
345+
origin_server_ts = parse_integer(request, "ts")
346+
if origin_server_ts is not None:
347+
event_dict["origin_server_ts"] = origin_server_ts
340348

341349
try:
342350
(

tests/rest/client/test_rooms.py

+117-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import json
2121
from http import HTTPStatus
2222
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
23-
from unittest.mock import Mock, call
23+
from unittest.mock import Mock, call, patch
2424
from urllib import parse as urlparse
2525

2626
from parameterized import param, parameterized
@@ -39,9 +39,10 @@
3939
RoomTypes,
4040
)
4141
from synapse.api.errors import Codes, HttpResponseException
42+
from synapse.appservice import ApplicationService
4243
from synapse.handlers.pagination import PurgeStatus
4344
from synapse.rest import admin
44-
from synapse.rest.client import account, directory, login, profile, room, sync
45+
from synapse.rest.client import account, directory, login, profile, register, room, sync
4546
from synapse.server import HomeServer
4647
from synapse.types import JsonDict, RoomAlias, UserID, create_requester
4748
from synapse.util import Clock
@@ -1252,6 +1253,120 @@ async def user_may_join_room(
12521253
)
12531254

12541255

1256+
class RoomAppserviceTsParamTestCase(unittest.HomeserverTestCase):
1257+
servlets = [
1258+
room.register_servlets,
1259+
synapse.rest.admin.register_servlets,
1260+
register.register_servlets,
1261+
]
1262+
1263+
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
1264+
self.appservice_user, _ = self.register_appservice_user(
1265+
"as_user_potato", self.appservice.token
1266+
)
1267+
1268+
# Create a room as the appservice user.
1269+
args = {
1270+
"access_token": self.appservice.token,
1271+
"user_id": self.appservice_user,
1272+
}
1273+
channel = self.make_request(
1274+
"POST",
1275+
f"/_matrix/client/r0/createRoom?{urlparse.urlencode(args)}",
1276+
content={"visibility": "public"},
1277+
)
1278+
1279+
assert channel.code == 200
1280+
self.room = channel.json_body["room_id"]
1281+
1282+
self.main_store = self.hs.get_datastores().main
1283+
1284+
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
1285+
config = self.default_config()
1286+
1287+
self.appservice = ApplicationService(
1288+
token="i_am_an_app_service",
1289+
id="1234",
1290+
namespaces={"users": [{"regex": r"@as_user.*", "exclusive": True}]},
1291+
# Note: this user does not have to match the regex above
1292+
sender="@as_main:test",
1293+
)
1294+
1295+
mock_load_appservices = Mock(return_value=[self.appservice])
1296+
with patch(
1297+
"synapse.storage.databases.main.appservice.load_appservices",
1298+
mock_load_appservices,
1299+
):
1300+
hs = self.setup_test_homeserver(config=config)
1301+
return hs
1302+
1303+
def test_send_event_ts(self) -> None:
1304+
"""Test sending a non-state event with a custom timestamp."""
1305+
ts = 1
1306+
1307+
url_params = {
1308+
"user_id": self.appservice_user,
1309+
"ts": ts,
1310+
}
1311+
channel = self.make_request(
1312+
"PUT",
1313+
path=f"/_matrix/client/r0/rooms/{self.room}/send/m.room.message/1234?"
1314+
+ urlparse.urlencode(url_params),
1315+
content={"body": "test", "msgtype": "m.text"},
1316+
access_token=self.appservice.token,
1317+
)
1318+
self.assertEqual(channel.code, 200, channel.json_body)
1319+
event_id = channel.json_body["event_id"]
1320+
1321+
# Ensure the event was persisted with the correct timestamp.
1322+
res = self.get_success(self.main_store.get_event(event_id))
1323+
self.assertEquals(ts, res.origin_server_ts)
1324+
1325+
def test_send_state_event_ts(self) -> None:
1326+
"""Test sending a state event with a custom timestamp."""
1327+
ts = 1
1328+
1329+
url_params = {
1330+
"user_id": self.appservice_user,
1331+
"ts": ts,
1332+
}
1333+
channel = self.make_request(
1334+
"PUT",
1335+
path=f"/_matrix/client/r0/rooms/{self.room}/state/m.room.name?"
1336+
+ urlparse.urlencode(url_params),
1337+
content={"name": "test"},
1338+
access_token=self.appservice.token,
1339+
)
1340+
self.assertEqual(channel.code, 200, channel.json_body)
1341+
event_id = channel.json_body["event_id"]
1342+
1343+
# Ensure the event was persisted with the correct timestamp.
1344+
res = self.get_success(self.main_store.get_event(event_id))
1345+
self.assertEquals(ts, res.origin_server_ts)
1346+
1347+
def test_send_membership_event_ts(self) -> None:
1348+
"""Test sending a membership event with a custom timestamp."""
1349+
ts = 1
1350+
1351+
url_params = {
1352+
"user_id": self.appservice_user,
1353+
"ts": ts,
1354+
}
1355+
channel = self.make_request(
1356+
"PUT",
1357+
path=f"/_matrix/client/r0/rooms/{self.room}/state/m.room.member/{self.appservice_user}?"
1358+
+ urlparse.urlencode(url_params),
1359+
content={"membership": "join", "display_name": "test"},
1360+
access_token=self.appservice.token,
1361+
)
1362+
self.assertEqual(channel.code, 200, channel.json_body)
1363+
event_id = channel.json_body["event_id"]
1364+
1365+
# Ensure the event was persisted with the correct timestamp.
1366+
res = self.get_success(self.main_store.get_event(event_id))
1367+
self.assertEquals(ts, res.origin_server_ts)
1368+
1369+
12551370
class RoomJoinRatelimitTestCase(RoomBase):
12561371
user_id = "@sid1:red"
12571372

0 commit comments

Comments
 (0)