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.
|
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
|
# Upgrading to v1.69.0
|
||||||
|
|
||||||
## Changes to the receipts replication streams
|
## Changes to the receipts replication streams
|
||||||
|
|
|
@ -172,12 +172,24 @@ class ApplicationService:
|
||||||
Returns:
|
Returns:
|
||||||
True if this service would like to know about this room.
|
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
|
room_id, on_invalidate=cache_context.invalidate
|
||||||
)
|
)
|
||||||
|
|
||||||
# check joined member events
|
# 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):
|
if self.is_interested_in_user(user_id):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -157,10 +157,23 @@ class ApplicationServiceWorkerStore(RoomMemberWorkerStore):
|
||||||
app_service: "ApplicationService",
|
app_service: "ApplicationService",
|
||||||
cache_context: _CacheContext,
|
cache_context: _CacheContext,
|
||||||
) -> List[str]:
|
) -> 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
|
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):
|
class ApplicationServiceStore(ApplicationServiceWorkerStore):
|
||||||
|
|
|
@ -152,6 +152,9 @@ class RoomMemberWorkerStore(EventsWorkerStore):
|
||||||
the forward extremities of those rooms will exclude most members. We may also
|
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
|
calculate room state incorrectly for such rooms and believe that a member is or
|
||||||
is not in the room when the opposite is true.
|
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(
|
return await self.db_pool.simple_select_onecol(
|
||||||
table="current_state_events",
|
table="current_state_events",
|
||||||
|
|
|
@ -43,7 +43,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
|
||||||
|
|
||||||
self.store = Mock()
|
self.store = Mock()
|
||||||
self.store.get_aliases_for_room = simple_async_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
|
@defer.inlineCallbacks
|
||||||
def test_regex_user_id_prefix_match(self):
|
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(
|
self.store.get_aliases_for_room = simple_async_mock(
|
||||||
["#irc_foobar:matrix.org", "#athing:matrix.org"]
|
["#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(
|
self.assertTrue(
|
||||||
(
|
(
|
||||||
yield defer.ensureDeferred(
|
yield defer.ensureDeferred(
|
||||||
|
@ -184,7 +184,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
|
||||||
self.store.get_aliases_for_room = simple_async_mock(
|
self.store.get_aliases_for_room = simple_async_mock(
|
||||||
["#xmpp_foobar:matrix.org", "#athing:matrix.org"]
|
["#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(
|
self.assertFalse(
|
||||||
(
|
(
|
||||||
yield defer.ensureDeferred(
|
yield defer.ensureDeferred(
|
||||||
|
@ -203,7 +203,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
|
||||||
self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*"))
|
self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*"))
|
||||||
self.event.sender = "@irc_foobar:matrix.org"
|
self.event.sender = "@irc_foobar:matrix.org"
|
||||||
self.store.get_aliases_for_room = simple_async_mock(["#irc_barfoo: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(
|
self.assertTrue(
|
||||||
(
|
(
|
||||||
yield defer.ensureDeferred(
|
yield defer.ensureDeferred(
|
||||||
|
@ -236,7 +236,7 @@ class ApplicationServiceTestCase(unittest.TestCase):
|
||||||
def test_member_list_match(self):
|
def test_member_list_match(self):
|
||||||
self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*"))
|
self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*"))
|
||||||
# Note that @irc_fo:here is the AS user.
|
# 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"]
|
["@alice:here", "@irc_fo:here", "@bob:here"]
|
||||||
)
|
)
|
||||||
self.store.get_aliases_for_room = simple_async_mock([])
|
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.rest.admin
|
||||||
import synapse.storage
|
import synapse.storage
|
||||||
from synapse.api.constants import EduTypes
|
from synapse.api.constants import EduTypes, EventTypes
|
||||||
from synapse.appservice import (
|
from synapse.appservice import (
|
||||||
ApplicationService,
|
ApplicationService,
|
||||||
TransactionOneTimeKeyCounts,
|
TransactionOneTimeKeyCounts,
|
||||||
|
@ -36,7 +36,7 @@ from synapse.util import Clock
|
||||||
from synapse.util.stringutils import random_string
|
from synapse.util.stringutils import random_string
|
||||||
|
|
||||||
from tests import unittest
|
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.unittest import override_config
|
||||||
from tests.utils import MockClock
|
from tests.utils import MockClock
|
||||||
|
|
||||||
|
@ -390,15 +390,16 @@ class ApplicationServicesHandlerSendEventsTestCase(unittest.HomeserverTestCase):
|
||||||
receipts.register_servlets,
|
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
|
# Mock the ApplicationServiceScheduler's _TransactionController's send method so that
|
||||||
# we can track any outgoing ephemeral events
|
# we can track any outgoing ephemeral events
|
||||||
self.send_mock = simple_async_mock()
|
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
|
# Mock out application services, and allow defining our own in tests
|
||||||
self._services: List[ApplicationService] = []
|
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
|
return_value=self._services
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -416,6 +417,157 @@ class ApplicationServicesHandlerSendEventsTestCase(unittest.HomeserverTestCase):
|
||||||
"exclusive_as_user", "password", self.exclusive_as_user_device_id
|
"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):
|
def test_sending_read_receipt_batches_to_application_services(self):
|
||||||
"""Tests that a large batch of read receipts are sent correctly to
|
"""Tests that a large batch of read receipts are sent correctly to
|
||||||
interested application services.
|
interested application services.
|
||||||
|
|
Loading…
Reference in New Issue