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

Commit 64ec45f

Browse files
Send to-device messages to application services (#11215)
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>
1 parent b7282fe commit 64ec45f

File tree

14 files changed

+856
-162
lines changed

14 files changed

+856
-162
lines changed

changelog.d/11215.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add experimental support for sending to-device messages to application services, as specified by [MSC2409](https://github.com/matrix-org/matrix-doc/pull/2409). Disabled by default.

synapse/appservice/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -351,11 +351,13 @@ def __init__(
351351
id: int,
352352
events: List[EventBase],
353353
ephemeral: List[JsonDict],
354+
to_device_messages: List[JsonDict],
354355
):
355356
self.service = service
356357
self.id = id
357358
self.events = events
358359
self.ephemeral = ephemeral
360+
self.to_device_messages = to_device_messages
359361

360362
async def send(self, as_api: "ApplicationServiceApi") -> bool:
361363
"""Sends this transaction using the provided AS API interface.
@@ -369,6 +371,7 @@ async def send(self, as_api: "ApplicationServiceApi") -> bool:
369371
service=self.service,
370372
events=self.events,
371373
ephemeral=self.ephemeral,
374+
to_device_messages=self.to_device_messages,
372375
txn_id=self.id,
373376
)
374377

synapse/appservice/api.py

+23-6
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,23 @@ async def push_bulk(
218218
service: "ApplicationService",
219219
events: List[EventBase],
220220
ephemeral: List[JsonDict],
221+
to_device_messages: List[JsonDict],
221222
txn_id: Optional[int] = None,
222223
) -> bool:
224+
"""
225+
Push data to an application service.
226+
227+
Args:
228+
service: The application service to send to.
229+
events: The persistent events to send.
230+
ephemeral: The ephemeral events to send.
231+
to_device_messages: The to-device messages to send.
232+
txn_id: An unique ID to assign to this transaction. Application services should
233+
deduplicate transactions received with identitical IDs.
234+
235+
Returns:
236+
True if the task succeeded, False if it failed.
237+
"""
223238
if service.url is None:
224239
return True
225240

@@ -237,13 +252,15 @@ async def push_bulk(
237252
uri = service.url + ("/transactions/%s" % urllib.parse.quote(str(txn_id)))
238253

239254
# Never send ephemeral events to appservices that do not support it
255+
body: Dict[str, List[JsonDict]] = {"events": serialized_events}
240256
if service.supports_ephemeral:
241-
body = {
242-
"events": serialized_events,
243-
"de.sorunome.msc2409.ephemeral": ephemeral,
244-
}
245-
else:
246-
body = {"events": serialized_events}
257+
body.update(
258+
{
259+
# TODO: Update to stable prefixes once MSC2409 completes FCP merge.
260+
"de.sorunome.msc2409.ephemeral": ephemeral,
261+
"de.sorunome.msc2409.to_device": to_device_messages,
262+
}
263+
)
247264

248265
try:
249266
await self.put_json(

synapse/appservice/scheduler.py

+75-22
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,16 @@
4848
components.
4949
"""
5050
import logging
51-
from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional, Set
51+
from typing import (
52+
TYPE_CHECKING,
53+
Awaitable,
54+
Callable,
55+
Collection,
56+
Dict,
57+
List,
58+
Optional,
59+
Set,
60+
)
5261

5362
from synapse.appservice import ApplicationService, ApplicationServiceState
5463
from synapse.appservice.api import ApplicationServiceApi
@@ -71,6 +80,9 @@
7180
# Maximum number of ephemeral events to provide in an AS transaction.
7281
MAX_EPHEMERAL_EVENTS_PER_TRANSACTION = 100
7382

83+
# Maximum number of to-device messages to provide in an AS transaction.
84+
MAX_TO_DEVICE_MESSAGES_PER_TRANSACTION = 100
85+
7486

7587
class ApplicationServiceScheduler:
7688
"""Public facing API for this module. Does the required DI to tie the
@@ -97,15 +109,40 @@ async def start(self) -> None:
97109
for service in services:
98110
self.txn_ctrl.start_recoverer(service)
99111

100-
def submit_event_for_as(
101-
self, service: ApplicationService, event: EventBase
112+
def enqueue_for_appservice(
113+
self,
114+
appservice: ApplicationService,
115+
events: Optional[Collection[EventBase]] = None,
116+
ephemeral: Optional[Collection[JsonDict]] = None,
117+
to_device_messages: Optional[Collection[JsonDict]] = None,
102118
) -> None:
103-
self.queuer.enqueue_event(service, event)
119+
"""
120+
Enqueue some data to be sent off to an application service.
104121
105-
def submit_ephemeral_events_for_as(
106-
self, service: ApplicationService, events: List[JsonDict]
107-
) -> None:
108-
self.queuer.enqueue_ephemeral(service, events)
122+
Args:
123+
appservice: The application service to create and send a transaction to.
124+
events: The persistent room events to send.
125+
ephemeral: The ephemeral events to send.
126+
to_device_messages: The to-device messages to send. These differ from normal
127+
to-device messages sent to clients, as they have 'to_device_id' and
128+
'to_user_id' fields.
129+
"""
130+
# We purposefully allow this method to run with empty events/ephemeral
131+
# collections, so that callers do not need to check iterable size themselves.
132+
if not events and not ephemeral and not to_device_messages:
133+
return
134+
135+
if events:
136+
self.queuer.queued_events.setdefault(appservice.id, []).extend(events)
137+
if ephemeral:
138+
self.queuer.queued_ephemeral.setdefault(appservice.id, []).extend(ephemeral)
139+
if to_device_messages:
140+
self.queuer.queued_to_device_messages.setdefault(appservice.id, []).extend(
141+
to_device_messages
142+
)
143+
144+
# Kick off a new application service transaction
145+
self.queuer.start_background_request(appservice)
109146

110147

111148
class _ServiceQueuer:
@@ -121,13 +158,15 @@ def __init__(self, txn_ctrl: "_TransactionController", clock: Clock):
121158
self.queued_events: Dict[str, List[EventBase]] = {}
122159
# dict of {service_id: [events]}
123160
self.queued_ephemeral: Dict[str, List[JsonDict]] = {}
161+
# dict of {service_id: [to_device_message_json]}
162+
self.queued_to_device_messages: Dict[str, List[JsonDict]] = {}
124163

125164
# the appservices which currently have a transaction in flight
126165
self.requests_in_flight: Set[str] = set()
127166
self.txn_ctrl = txn_ctrl
128167
self.clock = clock
129168

130-
def _start_background_request(self, service: ApplicationService) -> None:
169+
def start_background_request(self, service: ApplicationService) -> None:
131170
# start a sender for this appservice if we don't already have one
132171
if service.id in self.requests_in_flight:
133172
return
@@ -136,16 +175,6 @@ def _start_background_request(self, service: ApplicationService) -> None:
136175
"as-sender-%s" % (service.id,), self._send_request, service
137176
)
138177

139-
def enqueue_event(self, service: ApplicationService, event: EventBase) -> None:
140-
self.queued_events.setdefault(service.id, []).append(event)
141-
self._start_background_request(service)
142-
143-
def enqueue_ephemeral(
144-
self, service: ApplicationService, events: List[JsonDict]
145-
) -> None:
146-
self.queued_ephemeral.setdefault(service.id, []).extend(events)
147-
self._start_background_request(service)
148-
149178
async def _send_request(self, service: ApplicationService) -> None:
150179
# sanity-check: we shouldn't get here if this service already has a sender
151180
# running.
@@ -162,11 +191,21 @@ async def _send_request(self, service: ApplicationService) -> None:
162191
ephemeral = all_events_ephemeral[:MAX_EPHEMERAL_EVENTS_PER_TRANSACTION]
163192
del all_events_ephemeral[:MAX_EPHEMERAL_EVENTS_PER_TRANSACTION]
164193

165-
if not events and not ephemeral:
194+
all_to_device_messages = self.queued_to_device_messages.get(
195+
service.id, []
196+
)
197+
to_device_messages_to_send = all_to_device_messages[
198+
:MAX_TO_DEVICE_MESSAGES_PER_TRANSACTION
199+
]
200+
del all_to_device_messages[:MAX_TO_DEVICE_MESSAGES_PER_TRANSACTION]
201+
202+
if not events and not ephemeral and not to_device_messages_to_send:
166203
return
167204

168205
try:
169-
await self.txn_ctrl.send(service, events, ephemeral)
206+
await self.txn_ctrl.send(
207+
service, events, ephemeral, to_device_messages_to_send
208+
)
170209
except Exception:
171210
logger.exception("AS request failed")
172211
finally:
@@ -198,10 +237,24 @@ async def send(
198237
service: ApplicationService,
199238
events: List[EventBase],
200239
ephemeral: Optional[List[JsonDict]] = None,
240+
to_device_messages: Optional[List[JsonDict]] = None,
201241
) -> None:
242+
"""
243+
Create a transaction with the given data and send to the provided
244+
application service.
245+
246+
Args:
247+
service: The application service to send the transaction to.
248+
events: The persistent events to include in the transaction.
249+
ephemeral: The ephemeral events to include in the transaction.
250+
to_device_messages: The to-device messages to include in the transaction.
251+
"""
202252
try:
203253
txn = await self.store.create_appservice_txn(
204-
service=service, events=events, ephemeral=ephemeral or []
254+
service=service,
255+
events=events,
256+
ephemeral=ephemeral or [],
257+
to_device_messages=to_device_messages or [],
205258
)
206259
service_is_up = await self._is_service_up(service)
207260
if service_is_up:

synapse/config/experimental.py

+7
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,10 @@ def read_config(self, config: JsonDict, **kwargs):
5252
self.msc3202_device_masquerading_enabled: bool = experimental.get(
5353
"msc3202_device_masquerading", False
5454
)
55+
56+
# MSC2409 (this setting only relates to optionally sending to-device messages).
57+
# Presence, typing and read receipt EDUs are already sent to application services that
58+
# have opted in to receive them. If enabled, this adds to-device messages to that list.
59+
self.msc2409_to_device_messages_enabled: bool = experimental.get(
60+
"msc2409_to_device_messages_enabled", False
61+
)

0 commit comments

Comments
 (0)