Check appservice user interest against the local users instead of all users (`get_users_in_room` mis-use) (#13958)
This commit is contained in:
parent
67583281e3
commit
aa70556699
|
@ -0,0 +1 @@
|
|||
Check appservice user interest against the local users instead of all users in the room to align with [MSC3905](https://github.com/matrix-org/matrix-spec-proposals/pull/3905).
|
|
@ -97,6 +97,25 @@ As announced with the release of [Synapse 1.69.0](#deprecation-of-the-generate_s
|
|||
Modules relying on it can instead use the `create_login_token` method.
|
||||
|
||||
|
||||
## Changes to the events received by application services (interest)
|
||||
|
||||
To align with spec (changed in
|
||||
[MSC3905](https://github.com/matrix-org/matrix-spec-proposals/pull/3905)), Synapse now
|
||||
only considers local users to be interesting. In other words, the `users` namespace
|
||||
regex is only be applied against local users of the homeserver.
|
||||
|
||||
Please note, this probably doesn't affect the expected behavior of your application
|
||||
service, since an interesting local user in a room still means all messages in the room
|
||||
(from local or remote users) will still be considered interesting. And matching a room
|
||||
with the `rooms` or `aliases` namespace regex will still consider all events sent in the
|
||||
room to be interesting to the application service.
|
||||
|
||||
If one of your application service's `users` regex was intending to match a remote user,
|
||||
this will no longer match as you expect. The behavioral mismatch between matching all
|
||||
local users and some remote users is why the spec was changed/clarified and this
|
||||
caveat is no longer supported.
|
||||
|
||||
|
||||
# Upgrading to v1.69.0
|
||||
|
||||
## Changes to the receipts replication streams
|
||||
|
|
|
@ -172,12 +172,24 @@ class ApplicationService:
|
|||
Returns:
|
||||
True if this service would like to know about this room.
|
||||
"""
|
||||
member_list = await store.get_users_in_room(
|
||||
# We can use `get_local_users_in_room(...)` here because an application service
|
||||
# can only be interested in local users of the server it's on (ignore any remote
|
||||
# users that might match the user namespace regex).
|
||||
#
|
||||
# In the future, we can consider re-using
|
||||
# `store.get_app_service_users_in_room` which is very similar to this
|
||||
# function but has a slightly worse performance than this because we
|
||||
# have an early escape-hatch if we find a single user that the
|
||||
# appservice is interested in. The juice would be worth the squeeze if
|
||||
# `store.get_app_service_users_in_room` was used in more places besides
|
||||
# an experimental MSC. But for now we can avoid doing more work and
|
||||
# barely using it later.
|
||||
local_user_ids = await store.get_local_users_in_room(
|
||||
room_id, on_invalidate=cache_context.invalidate
|
||||
)
|
||||
|
||||
# check joined member events
|
||||
for user_id in member_list:
|
||||
for user_id in local_user_ids:
|
||||
if self.is_interested_in_user(user_id):
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -157,10 +157,23 @@ class ApplicationServiceWorkerStore(RoomMemberWorkerStore):
|
|||
app_service: "ApplicationService",
|
||||
cache_context: _CacheContext,
|
||||
) -> List[str]:
|
||||
users_in_room = await self.get_users_in_room(
|
||||
"""
|
||||
Get all users in a room that the appservice controls.
|
||||
|
||||
Args:
|
||||
room_id: The room to check in.
|
||||
app_service: The application service to check interest/control against
|
||||
|
||||
Returns:
|
||||
List of user IDs that the appservice controls.
|
||||
"""
|
||||
# We can use `get_local_users_in_room(...)` here because an application service
|
||||
# can only be interested in local users of the server it's on (ignore any remote
|
||||
# users that might match the user namespace regex).
|
||||
local_users_in_room = await self.get_local_users_in_room(
|
||||
room_id, on_invalidate=cache_context.invalidate
|
||||
)
|
||||
return list(filter(app_service.is_interested_in_user, users_in_room))
|
||||
return list(filter(app_service.is_interested_in_user, local_users_in_room))
|
||||
|
||||
|
||||
class ApplicationServiceStore(ApplicationServiceWorkerStore):
|
||||
|
|
|
@ -152,6 +152,9 @@ class RoomMemberWorkerStore(EventsWorkerStore):
|
|||
the forward extremities of those rooms will exclude most members. We may also
|
||||
calculate room state incorrectly for such rooms and believe that a member is or
|
||||
is not in the room when the opposite is true.
|
||||
|
||||
Note: If you only care about users in the room local to the homeserver, use
|
||||
`get_local_users_in_room(...)` instead which will be more performant.
|
||||
"""
|
||||
return await self.db_pool.simple_select_onecol(
|
||||
table="current_state_events",
|
||||
|
|
|
@ -43,7 +43,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
|
|||
|
||||
self.store = Mock()
|
||||
self.store.get_aliases_for_room = simple_async_mock([])
|
||||
self.store.get_users_in_room = simple_async_mock([])
|
||||
self.store.get_local_users_in_room = simple_async_mock([])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_regex_user_id_prefix_match(self):
|
||||
|
@ -129,7 +129,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
|
|||
self.store.get_aliases_for_room = simple_async_mock(
|
||||
["#irc_foobar:matrix.org", "#athing:matrix.org"]
|
||||
)
|
||||
self.store.get_users_in_room = simple_async_mock([])
|
||||
self.store.get_local_users_in_room = simple_async_mock([])
|
||||
self.assertTrue(
|
||||
(
|
||||
yield defer.ensureDeferred(
|
||||
|
@ -184,7 +184,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
|
|||
self.store.get_aliases_for_room = simple_async_mock(
|
||||
["#xmpp_foobar:matrix.org", "#athing:matrix.org"]
|
||||
)
|
||||
self.store.get_users_in_room = simple_async_mock([])
|
||||
self.store.get_local_users_in_room = simple_async_mock([])
|
||||
self.assertFalse(
|
||||
(
|
||||
yield defer.ensureDeferred(
|
||||
|
@ -203,7 +203,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
|
|||
self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*"))
|
||||
self.event.sender = "@irc_foobar:matrix.org"
|
||||
self.store.get_aliases_for_room = simple_async_mock(["#irc_barfoo:matrix.org"])
|
||||
self.store.get_users_in_room = simple_async_mock([])
|
||||
self.store.get_local_users_in_room = simple_async_mock([])
|
||||
self.assertTrue(
|
||||
(
|
||||
yield defer.ensureDeferred(
|
||||
|
@ -236,7 +236,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
|
|||
def test_member_list_match(self):
|
||||
self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*"))
|
||||
# Note that @irc_fo:here is the AS user.
|
||||
self.store.get_users_in_room = simple_async_mock(
|
||||
self.store.get_local_users_in_room = simple_async_mock(
|
||||
["@alice:here", "@irc_fo:here", "@bob:here"]
|
||||
)
|
||||
self.store.get_aliases_for_room = simple_async_mock([])
|
||||
|
|
|
@ -22,7 +22,7 @@ from twisted.test.proto_helpers import MemoryReactor
|
|||
|
||||
import synapse.rest.admin
|
||||
import synapse.storage
|
||||
from synapse.api.constants import EduTypes
|
||||
from synapse.api.constants import EduTypes, EventTypes
|
||||
from synapse.appservice import (
|
||||
ApplicationService,
|
||||
TransactionOneTimeKeyCounts,
|
||||
|
@ -36,7 +36,7 @@ from synapse.util import Clock
|
|||
from synapse.util.stringutils import random_string
|
||||
|
||||
from tests import unittest
|
||||
from tests.test_utils import make_awaitable, simple_async_mock
|
||||
from tests.test_utils import event_injection, make_awaitable, simple_async_mock
|
||||
from tests.unittest import override_config
|
||||
from tests.utils import MockClock
|
||||
|
||||
|
@ -390,15 +390,16 @@ class ApplicationServicesHandlerSendEventsTestCase(unittest.HomeserverTestCase):
|
|||
receipts.register_servlets,
|
||||
]
|
||||
|
||||
def prepare(self, reactor, clock, hs):
|
||||
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer):
|
||||
self.hs = hs
|
||||
# Mock the ApplicationServiceScheduler's _TransactionController's send method so that
|
||||
# we can track any outgoing ephemeral events
|
||||
self.send_mock = simple_async_mock()
|
||||
hs.get_application_service_handler().scheduler.txn_ctrl.send = self.send_mock
|
||||
hs.get_application_service_handler().scheduler.txn_ctrl.send = self.send_mock # type: ignore[assignment]
|
||||
|
||||
# Mock out application services, and allow defining our own in tests
|
||||
self._services: List[ApplicationService] = []
|
||||
self.hs.get_datastores().main.get_app_services = Mock(
|
||||
self.hs.get_datastores().main.get_app_services = Mock( # type: ignore[assignment]
|
||||
return_value=self._services
|
||||
)
|
||||
|
||||
|
@ -416,6 +417,157 @@ class ApplicationServicesHandlerSendEventsTestCase(unittest.HomeserverTestCase):
|
|||
"exclusive_as_user", "password", self.exclusive_as_user_device_id
|
||||
)
|
||||
|
||||
def _notify_interested_services(self):
|
||||
# This is normally set in `notify_interested_services` but we need to call the
|
||||
# internal async version so the reactor gets pushed to completion.
|
||||
self.hs.get_application_service_handler().current_max += 1
|
||||
self.get_success(
|
||||
self.hs.get_application_service_handler()._notify_interested_services(
|
||||
RoomStreamToken(
|
||||
None, self.hs.get_application_service_handler().current_max
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
("@local_as_user:test", True),
|
||||
# Defining remote users in an application service user namespace regex is a
|
||||
# footgun since the appservice might assume that it'll receive all events
|
||||
# sent by that remote user, but it will only receive events in rooms that
|
||||
# are shared with a local user. So we just remove this footgun possibility
|
||||
# entirely and we won't notify the application service based on remote
|
||||
# users.
|
||||
("@remote_as_user:remote", False),
|
||||
]
|
||||
)
|
||||
def test_match_interesting_room_members(
|
||||
self, interesting_user: str, should_notify: bool
|
||||
):
|
||||
"""
|
||||
Test to make sure that a interesting user (local or remote) in the room is
|
||||
notified as expected when someone else in the room sends a message.
|
||||
"""
|
||||
# Register an application service that's interested in the `interesting_user`
|
||||
interested_appservice = self._register_application_service(
|
||||
namespaces={
|
||||
ApplicationService.NS_USERS: [
|
||||
{
|
||||
"regex": interesting_user,
|
||||
"exclusive": False,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
# Create a room
|
||||
alice = self.register_user("alice", "pass")
|
||||
alice_access_token = self.login("alice", "pass")
|
||||
room_id = self.helper.create_room_as(room_creator=alice, tok=alice_access_token)
|
||||
|
||||
# Join the interesting user to the room
|
||||
self.get_success(
|
||||
event_injection.inject_member_event(
|
||||
self.hs, room_id, interesting_user, "join"
|
||||
)
|
||||
)
|
||||
# Kick the appservice into checking this membership event to get the event out
|
||||
# of the way
|
||||
self._notify_interested_services()
|
||||
# We don't care about the interesting user join event (this test is making sure
|
||||
# the next thing works)
|
||||
self.send_mock.reset_mock()
|
||||
|
||||
# Send a message from an uninteresting user
|
||||
self.helper.send_event(
|
||||
room_id,
|
||||
type=EventTypes.Message,
|
||||
content={
|
||||
"msgtype": "m.text",
|
||||
"body": "message from uninteresting user",
|
||||
},
|
||||
tok=alice_access_token,
|
||||
)
|
||||
# Kick the appservice into checking this new event
|
||||
self._notify_interested_services()
|
||||
|
||||
if should_notify:
|
||||
self.send_mock.assert_called_once()
|
||||
(
|
||||
service,
|
||||
events,
|
||||
_ephemeral,
|
||||
_to_device_messages,
|
||||
_otks,
|
||||
_fbks,
|
||||
_device_list_summary,
|
||||
) = self.send_mock.call_args[0]
|
||||
|
||||
# Even though the message came from an uninteresting user, it should still
|
||||
# notify us because the interesting user is joined to the room where the
|
||||
# message was sent.
|
||||
self.assertEqual(service, interested_appservice)
|
||||
self.assertEqual(events[0]["type"], "m.room.message")
|
||||
self.assertEqual(events[0]["sender"], alice)
|
||||
else:
|
||||
self.send_mock.assert_not_called()
|
||||
|
||||
def test_application_services_receive_events_sent_by_interesting_local_user(self):
|
||||
"""
|
||||
Test to make sure that a messages sent from a local user can be interesting and
|
||||
picked up by the appservice.
|
||||
"""
|
||||
# Register an application service that's interested in all local users
|
||||
interested_appservice = self._register_application_service(
|
||||
namespaces={
|
||||
ApplicationService.NS_USERS: [
|
||||
{
|
||||
"regex": ".*",
|
||||
"exclusive": False,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
# Create a room
|
||||
alice = self.register_user("alice", "pass")
|
||||
alice_access_token = self.login("alice", "pass")
|
||||
room_id = self.helper.create_room_as(room_creator=alice, tok=alice_access_token)
|
||||
|
||||
# We don't care about interesting events before this (this test is making sure
|
||||
# the next thing works)
|
||||
self.send_mock.reset_mock()
|
||||
|
||||
# Send a message from the interesting local user
|
||||
self.helper.send_event(
|
||||
room_id,
|
||||
type=EventTypes.Message,
|
||||
content={
|
||||
"msgtype": "m.text",
|
||||
"body": "message from interesting local user",
|
||||
},
|
||||
tok=alice_access_token,
|
||||
)
|
||||
# Kick the appservice into checking this new event
|
||||
self._notify_interested_services()
|
||||
|
||||
self.send_mock.assert_called_once()
|
||||
(
|
||||
service,
|
||||
events,
|
||||
_ephemeral,
|
||||
_to_device_messages,
|
||||
_otks,
|
||||
_fbks,
|
||||
_device_list_summary,
|
||||
) = self.send_mock.call_args[0]
|
||||
|
||||
# Events sent from an interesting local user should also be picked up as
|
||||
# interesting to the appservice.
|
||||
self.assertEqual(service, interested_appservice)
|
||||
self.assertEqual(events[0]["type"], "m.room.message")
|
||||
self.assertEqual(events[0]["sender"], alice)
|
||||
|
||||
def test_sending_read_receipt_batches_to_application_services(self):
|
||||
"""Tests that a large batch of read receipts are sent correctly to
|
||||
interested application services.
|
||||
|
|
Loading…
Reference in New Issue