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

Commit 2cc5ea9

Browse files
Add support for MSC3202: sending one-time key counts and fallback key usage states to Application Services. (#11617)
Co-authored-by: Erik Johnston <[email protected]>
1 parent 41cf4c2 commit 2cc5ea9

File tree

11 files changed

+528
-38
lines changed

11 files changed

+528
-38
lines changed

changelog.d/11617.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add support for MSC3202: sending one-time key counts and fallback key usage states to Application Services.

synapse/appservice/__init__.py

+16
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@
3131

3232
logger = logging.getLogger(__name__)
3333

34+
# Type for the `device_one_time_key_counts` field in an appservice transaction
35+
# user ID -> {device ID -> {algorithm -> count}}
36+
TransactionOneTimeKeyCounts = Dict[str, Dict[str, Dict[str, int]]]
37+
38+
# Type for the `device_unused_fallback_keys` field in an appservice transaction
39+
# user ID -> {device ID -> [algorithm]}
40+
TransactionUnusedFallbackKeys = Dict[str, Dict[str, List[str]]]
41+
3442

3543
class ApplicationServiceState(Enum):
3644
DOWN = "down"
@@ -72,6 +80,7 @@ def __init__(
7280
rate_limited: bool = True,
7381
ip_range_whitelist: Optional[IPSet] = None,
7482
supports_ephemeral: bool = False,
83+
msc3202_transaction_extensions: bool = False,
7584
):
7685
self.token = token
7786
self.url = (
@@ -84,6 +93,7 @@ def __init__(
8493
self.id = id
8594
self.ip_range_whitelist = ip_range_whitelist
8695
self.supports_ephemeral = supports_ephemeral
96+
self.msc3202_transaction_extensions = msc3202_transaction_extensions
8797

8898
if "|" in self.id:
8999
raise Exception("application service ID cannot contain '|' character")
@@ -339,12 +349,16 @@ def __init__(
339349
events: List[EventBase],
340350
ephemeral: List[JsonDict],
341351
to_device_messages: List[JsonDict],
352+
one_time_key_counts: TransactionOneTimeKeyCounts,
353+
unused_fallback_keys: TransactionUnusedFallbackKeys,
342354
):
343355
self.service = service
344356
self.id = id
345357
self.events = events
346358
self.ephemeral = ephemeral
347359
self.to_device_messages = to_device_messages
360+
self.one_time_key_counts = one_time_key_counts
361+
self.unused_fallback_keys = unused_fallback_keys
348362

349363
async def send(self, as_api: "ApplicationServiceApi") -> bool:
350364
"""Sends this transaction using the provided AS API interface.
@@ -359,6 +373,8 @@ async def send(self, as_api: "ApplicationServiceApi") -> bool:
359373
events=self.events,
360374
ephemeral=self.ephemeral,
361375
to_device_messages=self.to_device_messages,
376+
one_time_key_counts=self.one_time_key_counts,
377+
unused_fallback_keys=self.unused_fallback_keys,
362378
txn_id=self.id,
363379
)
364380

synapse/appservice/api.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@
1919

2020
from synapse.api.constants import EventTypes, Membership, ThirdPartyEntityKind
2121
from synapse.api.errors import CodeMessageException
22+
from synapse.appservice import (
23+
ApplicationService,
24+
TransactionOneTimeKeyCounts,
25+
TransactionUnusedFallbackKeys,
26+
)
2227
from synapse.events import EventBase
2328
from synapse.events.utils import serialize_event
2429
from synapse.http.client import SimpleHttpClient
2530
from synapse.types import JsonDict, ThirdPartyInstanceID
2631
from synapse.util.caches.response_cache import ResponseCache
2732

2833
if TYPE_CHECKING:
29-
from synapse.appservice import ApplicationService
3034
from synapse.server import HomeServer
3135

3236
logger = logging.getLogger(__name__)
@@ -219,6 +223,8 @@ async def push_bulk(
219223
events: List[EventBase],
220224
ephemeral: List[JsonDict],
221225
to_device_messages: List[JsonDict],
226+
one_time_key_counts: TransactionOneTimeKeyCounts,
227+
unused_fallback_keys: TransactionUnusedFallbackKeys,
222228
txn_id: Optional[int] = None,
223229
) -> bool:
224230
"""
@@ -252,7 +258,7 @@ async def push_bulk(
252258
uri = service.url + ("/transactions/%s" % urllib.parse.quote(str(txn_id)))
253259

254260
# Never send ephemeral events to appservices that do not support it
255-
body: Dict[str, List[JsonDict]] = {"events": serialized_events}
261+
body: JsonDict = {"events": serialized_events}
256262
if service.supports_ephemeral:
257263
body.update(
258264
{
@@ -262,6 +268,16 @@ async def push_bulk(
262268
}
263269
)
264270

271+
if service.msc3202_transaction_extensions:
272+
if one_time_key_counts:
273+
body[
274+
"org.matrix.msc3202.device_one_time_key_counts"
275+
] = one_time_key_counts
276+
if unused_fallback_keys:
277+
body[
278+
"org.matrix.msc3202.device_unused_fallback_keys"
279+
] = unused_fallback_keys
280+
265281
try:
266282
await self.put_json(
267283
uri=uri,

synapse/appservice/scheduler.py

+94-4
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,19 @@
5454
Callable,
5555
Collection,
5656
Dict,
57+
Iterable,
5758
List,
5859
Optional,
5960
Set,
61+
Tuple,
6062
)
6163

62-
from synapse.appservice import ApplicationService, ApplicationServiceState
64+
from synapse.appservice import (
65+
ApplicationService,
66+
ApplicationServiceState,
67+
TransactionOneTimeKeyCounts,
68+
TransactionUnusedFallbackKeys,
69+
)
6370
from synapse.appservice.api import ApplicationServiceApi
6471
from synapse.events import EventBase
6572
from synapse.logging.context import run_in_background
@@ -96,7 +103,7 @@ def __init__(self, hs: "HomeServer"):
96103
self.as_api = hs.get_application_service_api()
97104

98105
self.txn_ctrl = _TransactionController(self.clock, self.store, self.as_api)
99-
self.queuer = _ServiceQueuer(self.txn_ctrl, self.clock)
106+
self.queuer = _ServiceQueuer(self.txn_ctrl, self.clock, hs)
100107

101108
async def start(self) -> None:
102109
logger.info("Starting appservice scheduler")
@@ -153,7 +160,9 @@ class _ServiceQueuer:
153160
appservice at a given time.
154161
"""
155162

156-
def __init__(self, txn_ctrl: "_TransactionController", clock: Clock):
163+
def __init__(
164+
self, txn_ctrl: "_TransactionController", clock: Clock, hs: "HomeServer"
165+
):
157166
# dict of {service_id: [events]}
158167
self.queued_events: Dict[str, List[EventBase]] = {}
159168
# dict of {service_id: [events]}
@@ -165,6 +174,10 @@ def __init__(self, txn_ctrl: "_TransactionController", clock: Clock):
165174
self.requests_in_flight: Set[str] = set()
166175
self.txn_ctrl = txn_ctrl
167176
self.clock = clock
177+
self._msc3202_transaction_extensions_enabled: bool = (
178+
hs.config.experimental.msc3202_transaction_extensions
179+
)
180+
self._store = hs.get_datastores().main
168181

169182
def start_background_request(self, service: ApplicationService) -> None:
170183
# start a sender for this appservice if we don't already have one
@@ -202,15 +215,84 @@ async def _send_request(self, service: ApplicationService) -> None:
202215
if not events and not ephemeral and not to_device_messages_to_send:
203216
return
204217

218+
one_time_key_counts: Optional[TransactionOneTimeKeyCounts] = None
219+
unused_fallback_keys: Optional[TransactionUnusedFallbackKeys] = None
220+
221+
if (
222+
self._msc3202_transaction_extensions_enabled
223+
and service.msc3202_transaction_extensions
224+
):
225+
# Compute the one-time key counts and fallback key usage states
226+
# for the users which are mentioned in this transaction,
227+
# as well as the appservice's sender.
228+
(
229+
one_time_key_counts,
230+
unused_fallback_keys,
231+
) = await self._compute_msc3202_otk_counts_and_fallback_keys(
232+
service, events, ephemeral, to_device_messages_to_send
233+
)
234+
205235
try:
206236
await self.txn_ctrl.send(
207-
service, events, ephemeral, to_device_messages_to_send
237+
service,
238+
events,
239+
ephemeral,
240+
to_device_messages_to_send,
241+
one_time_key_counts,
242+
unused_fallback_keys,
208243
)
209244
except Exception:
210245
logger.exception("AS request failed")
211246
finally:
212247
self.requests_in_flight.discard(service.id)
213248

249+
async def _compute_msc3202_otk_counts_and_fallback_keys(
250+
self,
251+
service: ApplicationService,
252+
events: Iterable[EventBase],
253+
ephemerals: Iterable[JsonDict],
254+
to_device_messages: Iterable[JsonDict],
255+
) -> Tuple[TransactionOneTimeKeyCounts, TransactionUnusedFallbackKeys]:
256+
"""
257+
Given a list of the events, ephemeral messages and to-device messages,
258+
- first computes a list of application services users that may have
259+
interesting updates to the one-time key counts or fallback key usage.
260+
- then computes one-time key counts and fallback key usages for those users.
261+
Given a list of application service users that are interesting,
262+
compute one-time key counts and fallback key usages for the users.
263+
"""
264+
265+
# Set of 'interesting' users who may have updates
266+
users: Set[str] = set()
267+
268+
# The sender is always included
269+
users.add(service.sender)
270+
271+
# All AS users that would receive the PDUs or EDUs sent to these rooms
272+
# are classed as 'interesting'.
273+
rooms_of_interesting_users: Set[str] = set()
274+
# PDUs
275+
rooms_of_interesting_users.update(event.room_id for event in events)
276+
# EDUs
277+
rooms_of_interesting_users.update(
278+
ephemeral["room_id"] for ephemeral in ephemerals
279+
)
280+
281+
# Look up the AS users in those rooms
282+
for room_id in rooms_of_interesting_users:
283+
users.update(
284+
await self._store.get_app_service_users_in_room(room_id, service)
285+
)
286+
287+
# Add recipients of to-device messages.
288+
# device_message["user_id"] is the ID of the recipient.
289+
users.update(device_message["user_id"] for device_message in to_device_messages)
290+
291+
# Compute and return the counts / fallback key usage states
292+
otk_counts = await self._store.count_bulk_e2e_one_time_keys_for_as(users)
293+
unused_fbks = await self._store.get_e2e_bulk_unused_fallback_key_types(users)
294+
return otk_counts, unused_fbks
295+
214296

215297
class _TransactionController:
216298
"""Transaction manager.
@@ -238,6 +320,8 @@ async def send(
238320
events: List[EventBase],
239321
ephemeral: Optional[List[JsonDict]] = None,
240322
to_device_messages: Optional[List[JsonDict]] = None,
323+
one_time_key_counts: Optional[TransactionOneTimeKeyCounts] = None,
324+
unused_fallback_keys: Optional[TransactionUnusedFallbackKeys] = None,
241325
) -> None:
242326
"""
243327
Create a transaction with the given data and send to the provided
@@ -248,13 +332,19 @@ async def send(
248332
events: The persistent events to include in the transaction.
249333
ephemeral: The ephemeral events to include in the transaction.
250334
to_device_messages: The to-device messages to include in the transaction.
335+
one_time_key_counts: Counts of remaining one-time keys for relevant
336+
appservice devices in the transaction.
337+
unused_fallback_keys: Lists of unused fallback keys for relevant
338+
appservice devices in the transaction.
251339
"""
252340
try:
253341
txn = await self.store.create_appservice_txn(
254342
service=service,
255343
events=events,
256344
ephemeral=ephemeral or [],
257345
to_device_messages=to_device_messages or [],
346+
one_time_key_counts=one_time_key_counts or {},
347+
unused_fallback_keys=unused_fallback_keys or {},
258348
)
259349
service_is_up = await self._is_service_up(service)
260350
if service_is_up:

synapse/config/appservice.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,16 @@ def _load_appservice(
166166

167167
supports_ephemeral = as_info.get("de.sorunome.msc2409.push_ephemeral", False)
168168

169+
# Opt-in flag for the MSC3202-specific transactional behaviour.
170+
# When enabled, appservice transactions contain the following information:
171+
# - device One-Time Key counts
172+
# - device unused fallback key usage states
173+
msc3202_transaction_extensions = as_info.get("org.matrix.msc3202", False)
174+
if not isinstance(msc3202_transaction_extensions, bool):
175+
raise ValueError(
176+
"The `org.matrix.msc3202` option should be true or false if specified."
177+
)
178+
169179
return ApplicationService(
170180
token=as_info["as_token"],
171181
hostname=hostname,
@@ -174,8 +184,9 @@ def _load_appservice(
174184
hs_token=as_info["hs_token"],
175185
sender=user_id,
176186
id=as_info["id"],
177-
supports_ephemeral=supports_ephemeral,
178187
protocols=protocols,
179188
rate_limited=rate_limited,
180189
ip_range_whitelist=ip_range_whitelist,
190+
supports_ephemeral=supports_ephemeral,
191+
msc3202_transaction_extensions=msc3202_transaction_extensions,
181192
)

synapse/config/experimental.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,24 @@ def read_config(self, config: JsonDict, **kwargs):
4747
# MSC3030 (Jump to date API endpoint)
4848
self.msc3030_enabled: bool = experimental.get("msc3030_enabled", False)
4949

50-
# The portion of MSC3202 which is related to device masquerading.
51-
self.msc3202_device_masquerading_enabled: bool = experimental.get(
52-
"msc3202_device_masquerading", False
53-
)
54-
5550
# MSC2409 (this setting only relates to optionally sending to-device messages).
5651
# Presence, typing and read receipt EDUs are already sent to application services that
5752
# have opted in to receive them. If enabled, this adds to-device messages to that list.
5853
self.msc2409_to_device_messages_enabled: bool = experimental.get(
5954
"msc2409_to_device_messages_enabled", False
6055
)
6156

57+
# The portion of MSC3202 which is related to device masquerading.
58+
self.msc3202_device_masquerading_enabled: bool = experimental.get(
59+
"msc3202_device_masquerading", False
60+
)
61+
62+
# Portion of MSC3202 related to transaction extensions:
63+
# sending one-time key counts and fallback key usage to application services.
64+
self.msc3202_transaction_extensions: bool = experimental.get(
65+
"msc3202_transaction_extensions", False
66+
)
67+
6268
# MSC3706 (server-side support for partial state in /send_join responses)
6369
self.msc3706_enabled: bool = experimental.get("msc3706_enabled", False)
6470

0 commit comments

Comments
 (0)