Compare commits
16 Commits
clokep/sta
...
anoa/e2e_a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f8226af3a | ||
|
|
bd9d963af2 | ||
|
|
31c4b4093b | ||
|
|
8b0bbc1fb4 | ||
|
|
179dd5ae0c | ||
|
|
401cb2bbda | ||
|
|
8f1183cf7b | ||
|
|
ce020c30fc | ||
|
|
f65846b55b | ||
|
|
2930fe6fea | ||
|
|
e914f1d734 | ||
|
|
103f410bef | ||
|
|
7899f823ae | ||
|
|
78bd5eaa4f | ||
|
|
b7a44d4402 | ||
|
|
7fbfedb230 |
1
changelog.d/11215.feature
Normal file
1
changelog.d/11215.feature
Normal file
@@ -0,0 +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.
|
||||
@@ -48,7 +48,7 @@ This is all tied together by the AppServiceScheduler which DIs the required
|
||||
components.
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import Iterable, List, Optional
|
||||
|
||||
from synapse.appservice import ApplicationService, ApplicationServiceState
|
||||
from synapse.events import EventBase
|
||||
@@ -95,8 +95,20 @@ class ApplicationServiceScheduler:
|
||||
self.queuer.enqueue_event(service, event)
|
||||
|
||||
def submit_ephemeral_events_for_as(
|
||||
self, service: ApplicationService, events: List[JsonDict]
|
||||
):
|
||||
self, service: ApplicationService, events: Iterable[JsonDict]
|
||||
) -> None:
|
||||
"""
|
||||
Send ephemeral events to application services, and schedule a new
|
||||
outgoing AS transaction.
|
||||
|
||||
Args:
|
||||
service: The service to send ephemeral events to.
|
||||
events: The ephemeral events to send.
|
||||
"""
|
||||
# Ensure we have some events to send
|
||||
if not events:
|
||||
return
|
||||
|
||||
self.queuer.enqueue_ephemeral(service, events)
|
||||
|
||||
|
||||
@@ -130,7 +142,9 @@ class _ServiceQueuer:
|
||||
self.queued_events.setdefault(service.id, []).append(event)
|
||||
self._start_background_request(service)
|
||||
|
||||
def enqueue_ephemeral(self, service: ApplicationService, events: List[JsonDict]):
|
||||
def enqueue_ephemeral(
|
||||
self, service: ApplicationService, events: Iterable[JsonDict]
|
||||
) -> None:
|
||||
self.queued_ephemeral.setdefault(service.id, []).extend(events)
|
||||
self._start_background_request(service)
|
||||
|
||||
|
||||
@@ -46,3 +46,11 @@ class ExperimentalConfig(Config):
|
||||
|
||||
# MSC3266 (room summary api)
|
||||
self.msc3266_enabled: bool = experimental.get("msc3266_enabled", False)
|
||||
|
||||
# MSC2409 (this setting only relates to optionally sending to-device messages).
|
||||
# Presence, typing and read receipt EDUs are already sent to application services that
|
||||
# have opted in to receive them. This setting, if enabled, adds to-device messages
|
||||
# to that list.
|
||||
self.msc2409_to_device_messages_enabled: bool = experimental.get(
|
||||
"msc2409_to_device_messages_enabled", False
|
||||
)
|
||||
|
||||
@@ -55,6 +55,9 @@ class ApplicationServicesHandler:
|
||||
self.clock = hs.get_clock()
|
||||
self.notify_appservices = hs.config.appservice.notify_appservices
|
||||
self.event_sources = hs.get_event_sources()
|
||||
self._msc2409_to_device_messages_enabled = (
|
||||
hs.config.experimental.msc2409_to_device_messages_enabled
|
||||
)
|
||||
|
||||
self.current_max = 0
|
||||
self.is_processing = False
|
||||
@@ -199,8 +202,9 @@ class ApplicationServicesHandler:
|
||||
Args:
|
||||
stream_key: The stream the event came from.
|
||||
|
||||
`stream_key` can be "typing_key", "receipt_key" or "presence_key". Any other
|
||||
value for `stream_key` will cause this function to return early.
|
||||
`stream_key` can be "typing_key", "receipt_key", "presence_key" or
|
||||
"to_device_key". Any other value for `stream_key` will cause this function
|
||||
to return early.
|
||||
|
||||
Ephemeral events will only be pushed to appservices that have opted into
|
||||
receiving them by setting `push_ephemeral` to true in their registration
|
||||
@@ -216,8 +220,14 @@ class ApplicationServicesHandler:
|
||||
if not self.notify_appservices:
|
||||
return
|
||||
|
||||
# Ignore any unsupported streams
|
||||
if stream_key not in ("typing_key", "receipt_key", "presence_key"):
|
||||
# Notify appservices of updates in ephemeral event streams.
|
||||
# Only the following streams are currently supported.
|
||||
if stream_key not in (
|
||||
"typing_key",
|
||||
"receipt_key",
|
||||
"presence_key",
|
||||
"to_device_key",
|
||||
):
|
||||
return
|
||||
|
||||
# Assert that new_token is an integer (and not a RoomStreamToken).
|
||||
@@ -233,6 +243,13 @@ class ApplicationServicesHandler:
|
||||
# Additional context: https://github.com/matrix-org/synapse/pull/11137
|
||||
assert isinstance(new_token, int)
|
||||
|
||||
# Ignore to-device messages if the feature flag is not enabled
|
||||
if (
|
||||
stream_key == "to_device_key"
|
||||
and not self._msc2409_to_device_messages_enabled
|
||||
):
|
||||
return
|
||||
|
||||
# Check whether there are any appservices which have registered to receive
|
||||
# ephemeral events.
|
||||
#
|
||||
@@ -285,10 +302,7 @@ class ApplicationServicesHandler:
|
||||
):
|
||||
if stream_key == "receipt_key":
|
||||
events = await self._handle_receipts(service, new_token)
|
||||
if events:
|
||||
self.scheduler.submit_ephemeral_events_for_as(
|
||||
service, events
|
||||
)
|
||||
self.scheduler.submit_ephemeral_events_for_as(service, events)
|
||||
|
||||
# Persist the latest handled stream token for this appservice
|
||||
await self.store.set_type_stream_id_for_appservice(
|
||||
@@ -297,16 +311,28 @@ class ApplicationServicesHandler:
|
||||
|
||||
elif stream_key == "presence_key":
|
||||
events = await self._handle_presence(service, users, new_token)
|
||||
if events:
|
||||
self.scheduler.submit_ephemeral_events_for_as(
|
||||
service, events
|
||||
)
|
||||
self.scheduler.submit_ephemeral_events_for_as(service, events)
|
||||
|
||||
# Persist the latest handled stream token for this appservice
|
||||
await self.store.set_type_stream_id_for_appservice(
|
||||
service, "presence", new_token
|
||||
)
|
||||
|
||||
elif stream_key == "to_device_key":
|
||||
# Retrieve a list of to-device message events, as well as the
|
||||
# maximum stream token of the messages we were able to retrieve.
|
||||
to_device_messages = await self._get_to_device_messages(
|
||||
service, new_token, users
|
||||
)
|
||||
self.scheduler.submit_ephemeral_events_for_as(
|
||||
service, to_device_messages
|
||||
)
|
||||
|
||||
# Persist the latest handled stream token for this appservice
|
||||
await self.store.set_type_stream_id_for_appservice(
|
||||
service, "to_device", new_token
|
||||
)
|
||||
|
||||
async def _handle_typing(
|
||||
self, service: ApplicationService, new_token: int
|
||||
) -> List[JsonDict]:
|
||||
@@ -440,6 +466,76 @@ class ApplicationServicesHandler:
|
||||
|
||||
return events
|
||||
|
||||
async def _get_to_device_messages(
|
||||
self,
|
||||
service: ApplicationService,
|
||||
new_token: int,
|
||||
users: Collection[Union[str, UserID]],
|
||||
) -> List[JsonDict]:
|
||||
"""
|
||||
Given an application service, determine which events it should receive
|
||||
from those between the last-recorded typing event stream token for this
|
||||
appservice and the given stream token.
|
||||
|
||||
Args:
|
||||
service: The application service to check for which events it should receive.
|
||||
new_token: The latest to-device event stream token.
|
||||
users: The users that should receive new to-device messages.
|
||||
|
||||
Returns:
|
||||
A list of JSON dictionaries containing data derived from the typing events
|
||||
that should be sent to the given application service.
|
||||
"""
|
||||
# Get the stream token that this application service has processed up until
|
||||
from_key = await self.store.get_type_stream_id_for_appservice(
|
||||
service, "to_device"
|
||||
)
|
||||
|
||||
# Filter out users that this appservice is not interested in
|
||||
users_appservice_is_interested_in: List[str] = []
|
||||
for user in users:
|
||||
if isinstance(user, UserID):
|
||||
user = user.to_string()
|
||||
|
||||
if service.is_interested_in_user(user):
|
||||
users_appservice_is_interested_in.append(user)
|
||||
|
||||
if not users_appservice_is_interested_in:
|
||||
# Return early if the AS was not interested in any of these users
|
||||
return []
|
||||
|
||||
# Retrieve the to-device messages for each user
|
||||
recipient_user_id_device_id_to_messages = await self.store.get_new_messages(
|
||||
users_appservice_is_interested_in,
|
||||
from_key,
|
||||
new_token,
|
||||
)
|
||||
|
||||
# According to MSC2409, we'll need to add 'to_user_id' and 'to_device_id' fields
|
||||
# to the event JSON so that the application service will know which user/device
|
||||
# combination this messages was intended for.
|
||||
#
|
||||
# So we mangle this dict into a flat list of to-device messages with the relevant
|
||||
# user ID and device ID embedded inside each message dict.
|
||||
message_payload: List[JsonDict] = []
|
||||
for (
|
||||
user_id,
|
||||
device_id,
|
||||
), messages in recipient_user_id_device_id_to_messages.items():
|
||||
for message_json in messages:
|
||||
# Remove 'message_id' from the to-device message, as it's an internal ID
|
||||
message_json.pop("message_id", None)
|
||||
|
||||
message_payload.append(
|
||||
{
|
||||
"to_user_id": user_id,
|
||||
"to_device_id": device_id,
|
||||
**message_json,
|
||||
}
|
||||
)
|
||||
|
||||
return message_payload
|
||||
|
||||
async def query_user_exists(self, user_id: str) -> bool:
|
||||
"""Check if any application service knows this user_id exists.
|
||||
|
||||
|
||||
@@ -452,7 +452,9 @@ class Notifier:
|
||||
users,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error notifying application services of event")
|
||||
logger.exception(
|
||||
"Error notifying application services of ephemeral event"
|
||||
)
|
||||
|
||||
def on_new_replication_data(self) -> None:
|
||||
"""Used to inform replication listeners that something has happened
|
||||
|
||||
@@ -387,7 +387,7 @@ class ApplicationServiceTransactionWorkerStore(
|
||||
async def get_type_stream_id_for_appservice(
|
||||
self, service: ApplicationService, type: str
|
||||
) -> int:
|
||||
if type not in ("read_receipt", "presence"):
|
||||
if type not in ("read_receipt", "presence", "to_device"):
|
||||
raise ValueError(
|
||||
"Expected type to be a valid application stream id type, got %s"
|
||||
% (type,)
|
||||
@@ -414,7 +414,7 @@ class ApplicationServiceTransactionWorkerStore(
|
||||
async def set_type_stream_id_for_appservice(
|
||||
self, service: ApplicationService, stream_type: str, pos: Optional[int]
|
||||
) -> None:
|
||||
if stream_type not in ("read_receipt", "presence"):
|
||||
if stream_type not in ("read_receipt", "presence", "to_device"):
|
||||
raise ValueError(
|
||||
"Expected type to be a valid application stream id type, got %s"
|
||||
% (stream_type,)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Collection, Dict, List, Optional, Tuple
|
||||
|
||||
from synapse.logging import issue9533_logger
|
||||
from synapse.logging.opentracing import log_kv, set_tag, trace
|
||||
@@ -24,6 +24,7 @@ from synapse.storage.database import (
|
||||
DatabasePool,
|
||||
LoggingDatabaseConnection,
|
||||
LoggingTransaction,
|
||||
make_in_list_sql_clause,
|
||||
)
|
||||
from synapse.storage.engines import PostgresEngine
|
||||
from synapse.storage.util.id_generators import (
|
||||
@@ -136,6 +137,79 @@ class DeviceInboxWorkerStore(SQLBaseStore):
|
||||
def get_to_device_stream_token(self):
|
||||
return self._device_inbox_id_gen.get_current_token()
|
||||
|
||||
async def get_new_messages(
|
||||
self,
|
||||
user_ids: Collection[str],
|
||||
from_stream_id: int,
|
||||
to_stream_id: int,
|
||||
) -> Dict[Tuple[str, str], List[JsonDict]]:
|
||||
"""
|
||||
Retrieve to-device messages for a given set of user IDs.
|
||||
|
||||
Only to-device messages with stream ids between the given boundaries
|
||||
(from < X <= to) are returned.
|
||||
|
||||
Note that a stream ID can be shared by multiple copies of the same message with
|
||||
different recipient devices. Each (device, message_content) tuple has their own
|
||||
row in the device_inbox table.
|
||||
|
||||
Args:
|
||||
user_ids: The users to retrieve to-device messages for.
|
||||
from_stream_id: The lower boundary of stream id to filter with (exclusive).
|
||||
to_stream_id: The upper boundary of stream id to filter with (inclusive).
|
||||
|
||||
Returns:
|
||||
A list of to-device messages.
|
||||
"""
|
||||
# Bail out if none of these users have any messages
|
||||
for user_id in user_ids:
|
||||
if self._device_inbox_stream_cache.has_entity_changed(
|
||||
user_id, from_stream_id
|
||||
):
|
||||
break
|
||||
else:
|
||||
return {}
|
||||
|
||||
def get_new_messages_txn(txn: LoggingTransaction):
|
||||
# Build a query to select messages from any of the given users that are between
|
||||
# the given stream id bounds
|
||||
|
||||
# Scope to only the given users. We need to use this method as doing so is
|
||||
# different across database engines.
|
||||
many_clause_sql, many_clause_args = make_in_list_sql_clause(
|
||||
self.database_engine, "user_id", user_ids
|
||||
)
|
||||
|
||||
sql = f"""
|
||||
SELECT user_id, device_id, message_json FROM device_inbox
|
||||
WHERE {many_clause_sql}
|
||||
AND ? < stream_id AND stream_id <= ?
|
||||
ORDER BY stream_id ASC
|
||||
"""
|
||||
|
||||
txn.execute(sql, (*many_clause_args, from_stream_id, to_stream_id))
|
||||
|
||||
# Create a dictionary of (user ID, device ID) -> list of messages that
|
||||
# that device is meant to receive.
|
||||
recipient_user_id_device_id_to_messages: Dict[
|
||||
Tuple[str, str], List[JsonDict]
|
||||
] = {}
|
||||
|
||||
for row in txn:
|
||||
recipient_user_id = row[0]
|
||||
recipient_device_id = row[1]
|
||||
message_dict = db_to_json(row[2])
|
||||
|
||||
recipient_user_id_device_id_to_messages.setdefault(
|
||||
(recipient_user_id, recipient_device_id), []
|
||||
).append(message_dict)
|
||||
|
||||
return recipient_user_id_device_id_to_messages
|
||||
|
||||
return await self.db_pool.runInteraction(
|
||||
"get_new_messages", get_new_messages_txn
|
||||
)
|
||||
|
||||
async def get_new_messages_for_device(
|
||||
self,
|
||||
user_id: str,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/* Copyright 2021 The Matrix.org Foundation C.I.C
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
-- Add a column to track what to_device stream id that this application
|
||||
-- service has been caught up to.
|
||||
ALTER TABLE application_services_state ADD COLUMN to_device_stream_id BIGINT;
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright 2015, 2016 OpenMarket Ltd
|
||||
# Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -12,18 +12,23 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from typing import Dict, Iterable, List, Optional
|
||||
from unittest.mock import Mock
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
import synapse.rest.admin
|
||||
import synapse.storage
|
||||
from synapse.appservice import ApplicationService
|
||||
from synapse.handlers.appservice import ApplicationServicesHandler
|
||||
from synapse.rest.client import login, receipts, room, sendtodevice
|
||||
from synapse.types import RoomStreamToken
|
||||
from synapse.util.stringutils import random_string
|
||||
|
||||
from tests import unittest
|
||||
from tests.test_utils import make_awaitable
|
||||
from tests.utils import MockClock
|
||||
|
||||
from .. import unittest
|
||||
|
||||
|
||||
class AppServiceHandlerTestCase(unittest.TestCase):
|
||||
"""Tests the ApplicationServicesHandler."""
|
||||
@@ -261,7 +266,6 @@ class AppServiceHandlerTestCase(unittest.TestCase):
|
||||
"""
|
||||
interested_service = self._mkservice(is_interested=True)
|
||||
services = [interested_service]
|
||||
|
||||
self.mock_store.get_app_services.return_value = services
|
||||
self.mock_store.get_type_stream_id_for_appservice.return_value = make_awaitable(
|
||||
579
|
||||
@@ -305,7 +309,10 @@ class AppServiceHandlerTestCase(unittest.TestCase):
|
||||
self.handler.notify_interested_services_ephemeral(
|
||||
"receipt_key", 580, ["@fakerecipient:example.com"]
|
||||
)
|
||||
self.mock_scheduler.submit_ephemeral_events_for_as.assert_not_called()
|
||||
# This method will be called, but with an empty list of events
|
||||
self.mock_scheduler.submit_ephemeral_events_for_as.assert_called_once_with(
|
||||
interested_service, []
|
||||
)
|
||||
|
||||
def _mkservice(self, is_interested, protocols=None):
|
||||
service = Mock()
|
||||
@@ -321,3 +328,250 @@ class AppServiceHandlerTestCase(unittest.TestCase):
|
||||
service.token = "mock_service_token"
|
||||
service.url = "mock_service_url"
|
||||
return service
|
||||
|
||||
|
||||
class ApplicationServicesHandlerSendEventsTestCase(unittest.HomeserverTestCase):
|
||||
"""
|
||||
Tests that the ApplicationServicesHandler sends events to application
|
||||
services correctly.
|
||||
"""
|
||||
|
||||
servlets = [
|
||||
synapse.rest.admin.register_servlets_for_client_rest_resource,
|
||||
login.register_servlets,
|
||||
room.register_servlets,
|
||||
sendtodevice.register_servlets,
|
||||
receipts.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
# Mock the ApplicationServiceScheduler queuer so that we can track any
|
||||
# outgoing ephemeral events
|
||||
self.mock_service_queuer = Mock()
|
||||
self.mock_service_queuer.enqueue_ephemeral = Mock()
|
||||
hs.get_application_service_handler().scheduler.queuer = self.mock_service_queuer
|
||||
|
||||
# Mock out application services, and allow defining our own in tests
|
||||
self._services: List[ApplicationService] = []
|
||||
self.hs.get_datastore().get_app_services = Mock(return_value=self._services)
|
||||
|
||||
# A user on the homeserver.
|
||||
self.local_user_device_id = "local_device"
|
||||
self.local_user = self.register_user("local_user", "password")
|
||||
self.local_user_token = self.login(
|
||||
"local_user", "password", self.local_user_device_id
|
||||
)
|
||||
|
||||
# A user on the homeserver which lies within an appservice's exclusive user namespace.
|
||||
self.exclusive_as_user_device_id = "exclusive_as_device"
|
||||
self.exclusive_as_user = self.register_user("exclusive_as_user", "password")
|
||||
self.exclusive_as_user_token = self.login(
|
||||
"exclusive_as_user", "password", self.exclusive_as_user_device_id
|
||||
)
|
||||
|
||||
@unittest.override_config(
|
||||
{"experimental_features": {"msc2409_to_device_messages_enabled": True}}
|
||||
)
|
||||
def test_application_services_receive_local_to_device(self):
|
||||
"""
|
||||
Test that when a user sends a to-device message to another user
|
||||
that is an application service's user namespace, the
|
||||
application service will receive it.
|
||||
"""
|
||||
interested_appservice = self._register_application_service(
|
||||
namespaces={
|
||||
ApplicationService.NS_USERS: [
|
||||
{
|
||||
"regex": "@exclusive_as_user:.+",
|
||||
"exclusive": True,
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
# Have local_user send a to-device message to exclusive_as_user
|
||||
message_content = {"some_key": "some really interesting value"}
|
||||
chan = self.make_request(
|
||||
"PUT",
|
||||
"/_matrix/client/r0/sendToDevice/m.room_key_request/3",
|
||||
content={
|
||||
"messages": {
|
||||
self.exclusive_as_user: {
|
||||
self.exclusive_as_user_device_id: message_content
|
||||
}
|
||||
}
|
||||
},
|
||||
access_token=self.local_user_token,
|
||||
)
|
||||
self.assertEqual(chan.code, 200, chan.result)
|
||||
|
||||
# Have exclusive_as_user send a to-device message to local_user
|
||||
chan = self.make_request(
|
||||
"PUT",
|
||||
"/_matrix/client/r0/sendToDevice/m.room_key_request/4",
|
||||
content={
|
||||
"messages": {
|
||||
self.local_user: {self.local_user_device_id: message_content}
|
||||
}
|
||||
},
|
||||
access_token=self.exclusive_as_user_token,
|
||||
)
|
||||
self.assertEqual(chan.code, 200, chan.result)
|
||||
|
||||
# Check if our application service - that is interested in exclusive_as_user - received
|
||||
# the to-device message as part of an AS transaction.
|
||||
# Only the local_user -> exclusive_as_user to-device message should have been forwarded to the AS.
|
||||
#
|
||||
# The uninterested application service should not have been notified at all.
|
||||
self.mock_service_queuer.enqueue_ephemeral.assert_called_once()
|
||||
service, events = self.mock_service_queuer.enqueue_ephemeral.call_args[0]
|
||||
|
||||
# Assert that this was the same to-device message that local_user sent
|
||||
self.assertEqual(service, interested_appservice)
|
||||
self.assertEqual(events[0]["type"], "m.room_key_request")
|
||||
self.assertEqual(events[0]["sender"], self.local_user)
|
||||
|
||||
# Additional fields 'to_user_id' and 'to_device_id' specifically for
|
||||
# to-device messages via the AS API
|
||||
self.assertEqual(events[0]["to_user_id"], self.exclusive_as_user)
|
||||
self.assertEqual(events[0]["to_device_id"], self.exclusive_as_user_device_id)
|
||||
self.assertEqual(events[0]["content"], message_content)
|
||||
|
||||
@unittest.override_config(
|
||||
{"experimental_features": {"msc2409_to_device_messages_enabled": True}}
|
||||
)
|
||||
def test_application_services_receive_bursts_of_to_device(self):
|
||||
"""
|
||||
Test that when a user sends >100 to-device messages at once, any
|
||||
interested AS's will receive them in separate transactions.
|
||||
|
||||
Also tests that uninterested application services do not receive messages.
|
||||
"""
|
||||
# Register two application services with exclusive interest in a user
|
||||
interested_appservices = []
|
||||
for _ in range(2):
|
||||
appservice = self._register_application_service(
|
||||
namespaces={
|
||||
ApplicationService.NS_USERS: [
|
||||
{
|
||||
"regex": "@exclusive_as_user:.+",
|
||||
"exclusive": True,
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
interested_appservices.append(appservice)
|
||||
|
||||
# ...and an application service which does not have any user interest.
|
||||
self._register_application_service()
|
||||
|
||||
to_device_message_content = {
|
||||
"some key": "some interesting value",
|
||||
}
|
||||
|
||||
# We need to send a large burst of to-device messages. We also would like to
|
||||
# include them all in the same application service transaction so that we can
|
||||
# test large transactions.
|
||||
#
|
||||
# To do this, we can send a single to-device message to many user devices at
|
||||
# once.
|
||||
#
|
||||
# We insert number_of_messages - 1 messages into the database directly. We'll then
|
||||
# send a final to-device message to the real device, which will also kick off
|
||||
# an AS transaction (as just inserting messages into the DB won't).
|
||||
number_of_messages = 150
|
||||
fake_device_ids = [f"device_{num}" for num in range(number_of_messages - 1)]
|
||||
messages = {
|
||||
self.exclusive_as_user: {
|
||||
device_id: to_device_message_content for device_id in fake_device_ids
|
||||
}
|
||||
}
|
||||
|
||||
# Create a fake device per message. We can't send to-device messages to
|
||||
# a device that doesn't exist.
|
||||
self.get_success(
|
||||
self.hs.get_datastore().db_pool.simple_insert_many(
|
||||
desc="test_application_services_receive_burst_of_to_device",
|
||||
table="devices",
|
||||
values=[
|
||||
{
|
||||
"user_id": self.exclusive_as_user,
|
||||
"device_id": device_id,
|
||||
}
|
||||
for device_id in fake_device_ids
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
# Seed the device_inbox table with our fake messages
|
||||
self.get_success(
|
||||
self.hs.get_datastore().add_messages_to_device_inbox(messages, {})
|
||||
)
|
||||
|
||||
# Now have local_user send a final to-device message to exclusive_as_user. All unsent
|
||||
# to-device messages should be sent to any application services
|
||||
# interested in exclusive_as_user.
|
||||
chan = self.make_request(
|
||||
"PUT",
|
||||
"/_matrix/client/r0/sendToDevice/m.room_key_request/4",
|
||||
content={
|
||||
"messages": {
|
||||
self.exclusive_as_user: {
|
||||
self.exclusive_as_user_device_id: to_device_message_content
|
||||
}
|
||||
}
|
||||
},
|
||||
access_token=self.local_user_token,
|
||||
)
|
||||
self.assertEqual(chan.code, 200, chan.result)
|
||||
|
||||
self.mock_service_queuer.enqueue_ephemeral.assert_called()
|
||||
|
||||
# Count the total number of to-device messages that were sent out per-service.
|
||||
# Ensure that we only sent to-device messages to interested services, and that
|
||||
# each interested service received the full count of to-device messages.
|
||||
service_id_to_message_count: Dict[str, int] = {}
|
||||
|
||||
for call in self.mock_service_queuer.enqueue_ephemeral.call_args_list:
|
||||
service, events = call[0]
|
||||
|
||||
# Check that this was made to an interested service
|
||||
self.assertIn(service, interested_appservices)
|
||||
|
||||
# Add to the count of messages for this application service
|
||||
service_id_to_message_count.setdefault(service.id, 0)
|
||||
service_id_to_message_count[service.id] += len(events)
|
||||
|
||||
# Assert that each interested service received the full count of messages
|
||||
for count in service_id_to_message_count.values():
|
||||
self.assertEqual(count, number_of_messages)
|
||||
|
||||
def _register_application_service(
|
||||
self,
|
||||
namespaces: Optional[Dict[str, Iterable[Dict]]] = None,
|
||||
) -> ApplicationService:
|
||||
"""
|
||||
Register a new application service, with the given namespaces of interest.
|
||||
|
||||
Args:
|
||||
namespaces: A dictionary containing any user, room or alias namespaces that
|
||||
the application service is interested in.
|
||||
|
||||
Returns:
|
||||
The registered application service.
|
||||
"""
|
||||
# Create an application service
|
||||
appservice = ApplicationService(
|
||||
token=None,
|
||||
hostname="example.com",
|
||||
id=random_string(10),
|
||||
sender="@as:example.com",
|
||||
rate_limited=False,
|
||||
namespaces=namespaces,
|
||||
supports_ephemeral=True,
|
||||
)
|
||||
|
||||
# Register the application service
|
||||
self._services.append(appservice)
|
||||
|
||||
return appservice
|
||||
|
||||
Reference in New Issue
Block a user