mirror of
https://github.com/element-hq/synapse.git
synced 2025-03-15 20:20:18 +00:00
Bring auto-accept invite logic into Synapse (#17147)
This PR ports the logic from the [synapse_auto_accept_invite](https://github.com/matrix-org/synapse-auto-accept-invite) module into synapse. I went with the naive approach of injecting the "module" next to where third party modules are currently loaded. If there is a better/preferred way to handle this, I'm all ears. It wasn't obvious to me if there was a better location to add this logic that would cleanly apply to all incoming invite events. Relies on https://github.com/element-hq/synapse/pull/17166 to fix linter errors.
This commit is contained in:
parent
b5facbac0f
commit
6a9a641fb8
11 changed files with 945 additions and 1 deletions
1
changelog.d/17147.feature
Normal file
1
changelog.d/17147.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add the ability to auto-accept invites on the behalf of users. See the [`auto_accept_invites`](https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html#auto-accept-invites) config option for details.
|
|
@ -4595,3 +4595,32 @@ background_updates:
|
||||||
min_batch_size: 10
|
min_batch_size: 10
|
||||||
default_batch_size: 50
|
default_batch_size: 50
|
||||||
```
|
```
|
||||||
|
---
|
||||||
|
## Auto Accept Invites
|
||||||
|
Configuration settings related to automatically accepting invites.
|
||||||
|
|
||||||
|
---
|
||||||
|
### `auto_accept_invites`
|
||||||
|
|
||||||
|
Automatically accepting invites controls whether users are presented with an invite request or if they
|
||||||
|
are instead automatically joined to a room when receiving an invite. Set the `enabled` sub-option to true to
|
||||||
|
enable auto-accepting invites. Defaults to false.
|
||||||
|
This setting has the following sub-options:
|
||||||
|
* `enabled`: Whether to run the auto-accept invites logic. Defaults to false.
|
||||||
|
* `only_for_direct_messages`: Whether invites should be automatically accepted for all room types, or only
|
||||||
|
for direct messages. Defaults to false.
|
||||||
|
* `only_from_local_users`: Whether to only automatically accept invites from users on this homeserver. Defaults to false.
|
||||||
|
* `worker_to_run_on`: Which worker to run this module on. This must match the "worker_name".
|
||||||
|
|
||||||
|
NOTE: Care should be taken not to enable this setting if the `synapse_auto_accept_invite` module is enabled and installed.
|
||||||
|
The two modules will compete to perform the same task and may result in undesired behaviour. For example, multiple join
|
||||||
|
events could be generated from a single invite.
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
```yaml
|
||||||
|
auto_accept_invites:
|
||||||
|
enabled: true
|
||||||
|
only_for_direct_messages: true
|
||||||
|
only_from_local_users: true
|
||||||
|
worker_to_run_on: "worker_1"
|
||||||
|
```
|
||||||
|
|
|
@ -68,6 +68,7 @@ from synapse.config._base import format_config_error
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
from synapse.config.server import ListenerConfig, ManholeConfig, TCPListenerConfig
|
from synapse.config.server import ListenerConfig, ManholeConfig, TCPListenerConfig
|
||||||
from synapse.crypto import context_factory
|
from synapse.crypto import context_factory
|
||||||
|
from synapse.events.auto_accept_invites import InviteAutoAccepter
|
||||||
from synapse.events.presence_router import load_legacy_presence_router
|
from synapse.events.presence_router import load_legacy_presence_router
|
||||||
from synapse.handlers.auth import load_legacy_password_auth_providers
|
from synapse.handlers.auth import load_legacy_password_auth_providers
|
||||||
from synapse.http.site import SynapseSite
|
from synapse.http.site import SynapseSite
|
||||||
|
@ -582,6 +583,11 @@ async def start(hs: "HomeServer") -> None:
|
||||||
m = module(config, module_api)
|
m = module(config, module_api)
|
||||||
logger.info("Loaded module %s", m)
|
logger.info("Loaded module %s", m)
|
||||||
|
|
||||||
|
if hs.config.auto_accept_invites.enabled:
|
||||||
|
# Start the local auto_accept_invites module.
|
||||||
|
m = InviteAutoAccepter(hs.config.auto_accept_invites, module_api)
|
||||||
|
logger.info("Loaded local module %s", m)
|
||||||
|
|
||||||
load_legacy_spam_checkers(hs)
|
load_legacy_spam_checkers(hs)
|
||||||
load_legacy_third_party_event_rules(hs)
|
load_legacy_third_party_event_rules(hs)
|
||||||
load_legacy_presence_router(hs)
|
load_legacy_presence_router(hs)
|
||||||
|
|
|
@ -23,6 +23,7 @@ from synapse.config import ( # noqa: F401
|
||||||
api,
|
api,
|
||||||
appservice,
|
appservice,
|
||||||
auth,
|
auth,
|
||||||
|
auto_accept_invites,
|
||||||
background_updates,
|
background_updates,
|
||||||
cache,
|
cache,
|
||||||
captcha,
|
captcha,
|
||||||
|
@ -120,6 +121,7 @@ class RootConfig:
|
||||||
federation: federation.FederationConfig
|
federation: federation.FederationConfig
|
||||||
retention: retention.RetentionConfig
|
retention: retention.RetentionConfig
|
||||||
background_updates: background_updates.BackgroundUpdateConfig
|
background_updates: background_updates.BackgroundUpdateConfig
|
||||||
|
auto_accept_invites: auto_accept_invites.AutoAcceptInvitesConfig
|
||||||
|
|
||||||
config_classes: List[Type["Config"]] = ...
|
config_classes: List[Type["Config"]] = ...
|
||||||
config_files: List[str]
|
config_files: List[str]
|
||||||
|
|
43
synapse/config/auto_accept_invites.py
Normal file
43
synapse/config/auto_accept_invites.py
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
# Originally licensed under the Apache License, Version 2.0:
|
||||||
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# [This file includes modifications made by New Vector Limited]
|
||||||
|
#
|
||||||
|
#
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from synapse.types import JsonDict
|
||||||
|
|
||||||
|
from ._base import Config
|
||||||
|
|
||||||
|
|
||||||
|
class AutoAcceptInvitesConfig(Config):
|
||||||
|
section = "auto_accept_invites"
|
||||||
|
|
||||||
|
def read_config(self, config: JsonDict, **kwargs: Any) -> None:
|
||||||
|
auto_accept_invites_config = config.get("auto_accept_invites") or {}
|
||||||
|
|
||||||
|
self.enabled = auto_accept_invites_config.get("enabled", False)
|
||||||
|
|
||||||
|
self.accept_invites_only_for_direct_messages = auto_accept_invites_config.get(
|
||||||
|
"only_for_direct_messages", False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.accept_invites_only_from_local_users = auto_accept_invites_config.get(
|
||||||
|
"only_from_local_users", False
|
||||||
|
)
|
||||||
|
|
||||||
|
self.worker_to_run_on = auto_accept_invites_config.get("worker_to_run_on")
|
|
@ -23,6 +23,7 @@ from .account_validity import AccountValidityConfig
|
||||||
from .api import ApiConfig
|
from .api import ApiConfig
|
||||||
from .appservice import AppServiceConfig
|
from .appservice import AppServiceConfig
|
||||||
from .auth import AuthConfig
|
from .auth import AuthConfig
|
||||||
|
from .auto_accept_invites import AutoAcceptInvitesConfig
|
||||||
from .background_updates import BackgroundUpdateConfig
|
from .background_updates import BackgroundUpdateConfig
|
||||||
from .cache import CacheConfig
|
from .cache import CacheConfig
|
||||||
from .captcha import CaptchaConfig
|
from .captcha import CaptchaConfig
|
||||||
|
@ -105,4 +106,5 @@ class HomeServerConfig(RootConfig):
|
||||||
RedisConfig,
|
RedisConfig,
|
||||||
ExperimentalConfig,
|
ExperimentalConfig,
|
||||||
BackgroundUpdateConfig,
|
BackgroundUpdateConfig,
|
||||||
|
AutoAcceptInvitesConfig,
|
||||||
]
|
]
|
||||||
|
|
196
synapse/events/auto_accept_invites.py
Normal file
196
synapse/events/auto_accept_invites.py
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright 2021 The Matrix.org Foundation C.I.C
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
# Originally licensed under the Apache License, Version 2.0:
|
||||||
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# [This file includes modifications made by New Vector Limited]
|
||||||
|
#
|
||||||
|
#
|
||||||
|
import logging
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any, Dict, Tuple
|
||||||
|
|
||||||
|
from synapse.api.constants import AccountDataTypes, EventTypes, Membership
|
||||||
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.config.auto_accept_invites import AutoAcceptInvitesConfig
|
||||||
|
from synapse.module_api import EventBase, ModuleApi, run_as_background_process
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class InviteAutoAccepter:
|
||||||
|
def __init__(self, config: AutoAcceptInvitesConfig, api: ModuleApi):
|
||||||
|
# Keep a reference to the Module API.
|
||||||
|
self._api = api
|
||||||
|
self._config = config
|
||||||
|
|
||||||
|
if not self._config.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
should_run_on_this_worker = config.worker_to_run_on == self._api.worker_name
|
||||||
|
|
||||||
|
if not should_run_on_this_worker:
|
||||||
|
logger.info(
|
||||||
|
"Not accepting invites on this worker (configured: %r, here: %r)",
|
||||||
|
config.worker_to_run_on,
|
||||||
|
self._api.worker_name,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Accepting invites on this worker (here: %r)", self._api.worker_name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register the callback.
|
||||||
|
self._api.register_third_party_rules_callbacks(
|
||||||
|
on_new_event=self.on_new_event,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_new_event(self, event: EventBase, *args: Any) -> None:
|
||||||
|
"""Listens for new events, and if the event is an invite for a local user then
|
||||||
|
automatically accepts it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: The incoming event.
|
||||||
|
"""
|
||||||
|
# Check if the event is an invite for a local user.
|
||||||
|
is_invite_for_local_user = (
|
||||||
|
event.type == EventTypes.Member
|
||||||
|
and event.is_state()
|
||||||
|
and event.membership == Membership.INVITE
|
||||||
|
and self._api.is_mine(event.state_key)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only accept invites for direct messages if the configuration mandates it.
|
||||||
|
is_direct_message = event.content.get("is_direct", False)
|
||||||
|
is_allowed_by_direct_message_rules = (
|
||||||
|
not self._config.accept_invites_only_for_direct_messages
|
||||||
|
or is_direct_message is True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Only accept invites from remote users if the configuration mandates it.
|
||||||
|
is_from_local_user = self._api.is_mine(event.sender)
|
||||||
|
is_allowed_by_local_user_rules = (
|
||||||
|
not self._config.accept_invites_only_from_local_users
|
||||||
|
or is_from_local_user is True
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
is_invite_for_local_user
|
||||||
|
and is_allowed_by_direct_message_rules
|
||||||
|
and is_allowed_by_local_user_rules
|
||||||
|
):
|
||||||
|
# Make the user join the room. We run this as a background process to circumvent a race condition
|
||||||
|
# that occurs when responding to invites over federation (see https://github.com/matrix-org/synapse-auto-accept-invite/issues/12)
|
||||||
|
run_as_background_process(
|
||||||
|
"retry_make_join",
|
||||||
|
self._retry_make_join,
|
||||||
|
event.state_key,
|
||||||
|
event.state_key,
|
||||||
|
event.room_id,
|
||||||
|
"join",
|
||||||
|
bg_start_span=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_direct_message:
|
||||||
|
# Mark this room as a direct message!
|
||||||
|
await self._mark_room_as_direct_message(
|
||||||
|
event.state_key, event.sender, event.room_id
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _mark_room_as_direct_message(
|
||||||
|
self, user_id: str, dm_user_id: str, room_id: str
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Marks a room (`room_id`) as a direct message with the counterparty `dm_user_id`
|
||||||
|
from the perspective of the user `user_id`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: the user for whom the membership is changing
|
||||||
|
dm_user_id: the user performing the membership change
|
||||||
|
room_id: room id of the room the user is invited to
|
||||||
|
"""
|
||||||
|
|
||||||
|
# This is a dict of User IDs to tuples of Room IDs
|
||||||
|
# (get_global will return a frozendict of tuples as it freezes the data,
|
||||||
|
# but we should accept either frozen or unfrozen variants.)
|
||||||
|
# Be careful: we convert the outer frozendict into a dict here,
|
||||||
|
# but the contents of the dict are still frozen (tuples in lieu of lists,
|
||||||
|
# etc.)
|
||||||
|
dm_map: Dict[str, Tuple[str, ...]] = dict(
|
||||||
|
await self._api.account_data_manager.get_global(
|
||||||
|
user_id, AccountDataTypes.DIRECT
|
||||||
|
)
|
||||||
|
or {}
|
||||||
|
)
|
||||||
|
|
||||||
|
if dm_user_id not in dm_map:
|
||||||
|
dm_map[dm_user_id] = (room_id,)
|
||||||
|
else:
|
||||||
|
dm_rooms_for_user = dm_map[dm_user_id]
|
||||||
|
assert isinstance(dm_rooms_for_user, (tuple, list))
|
||||||
|
|
||||||
|
dm_map[dm_user_id] = tuple(dm_rooms_for_user) + (room_id,)
|
||||||
|
|
||||||
|
await self._api.account_data_manager.put_global(
|
||||||
|
user_id, AccountDataTypes.DIRECT, dm_map
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _retry_make_join(
|
||||||
|
self, sender: str, target: str, room_id: str, new_membership: str
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
A function to retry sending the `make_join` request with an increasing backoff. This is
|
||||||
|
implemented to work around a race condition when receiving invites over federation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sender: the user performing the membership change
|
||||||
|
target: the user for whom the membership is changing
|
||||||
|
room_id: room id of the room to join to
|
||||||
|
new_membership: the type of membership event (in this case will be "join")
|
||||||
|
"""
|
||||||
|
|
||||||
|
sleep = 0
|
||||||
|
retries = 0
|
||||||
|
join_event = None
|
||||||
|
|
||||||
|
while retries < 5:
|
||||||
|
try:
|
||||||
|
await self._api.sleep(sleep)
|
||||||
|
join_event = await self._api.update_room_membership(
|
||||||
|
sender=sender,
|
||||||
|
target=target,
|
||||||
|
room_id=room_id,
|
||||||
|
new_membership=new_membership,
|
||||||
|
)
|
||||||
|
except SynapseError as e:
|
||||||
|
if e.code == HTTPStatus.FORBIDDEN:
|
||||||
|
logger.debug(
|
||||||
|
f"Update_room_membership was forbidden. This can sometimes be expected for remote invites. Exception: {e}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.warn(
|
||||||
|
f"Update_room_membership raised the following unexpected (SynapseError) exception: {e}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(
|
||||||
|
f"Update_room_membership raised the following unexpected exception: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
sleep = 2**retries
|
||||||
|
retries += 1
|
||||||
|
|
||||||
|
if join_event is not None:
|
||||||
|
break
|
|
@ -817,7 +817,7 @@ class SsoHandler:
|
||||||
server_name = profile["avatar_url"].split("/")[-2]
|
server_name = profile["avatar_url"].split("/")[-2]
|
||||||
media_id = profile["avatar_url"].split("/")[-1]
|
media_id = profile["avatar_url"].split("/")[-1]
|
||||||
if self._is_mine_server_name(server_name):
|
if self._is_mine_server_name(server_name):
|
||||||
media = await self._media_repo.store.get_local_media(media_id)
|
media = await self._media_repo.store.get_local_media(media_id) # type: ignore[has-type]
|
||||||
if media is not None and upload_name == media.upload_name:
|
if media is not None and upload_name == media.upload_name:
|
||||||
logger.info("skipping saving the user avatar")
|
logger.info("skipping saving the user avatar")
|
||||||
return True
|
return True
|
||||||
|
|
657
tests/events/test_auto_accept_invites.py
Normal file
657
tests/events/test_auto_accept_invites.py
Normal file
|
@ -0,0 +1,657 @@
|
||||||
|
#
|
||||||
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
||||||
|
#
|
||||||
|
# Copyright 2021 The Matrix.org Foundation C.I.C
|
||||||
|
# Copyright (C) 2024 New Vector, Ltd
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# See the GNU Affero General Public License for more details:
|
||||||
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
||||||
|
#
|
||||||
|
# Originally licensed under the Apache License, Version 2.0:
|
||||||
|
# <http://www.apache.org/licenses/LICENSE-2.0>.
|
||||||
|
#
|
||||||
|
# [This file includes modifications made by New Vector Limited]
|
||||||
|
#
|
||||||
|
#
|
||||||
|
import asyncio
|
||||||
|
from asyncio import Future
|
||||||
|
from http import HTTPStatus
|
||||||
|
from typing import Any, Awaitable, Dict, List, Optional, Tuple, TypeVar, cast
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
import attr
|
||||||
|
from parameterized import parameterized
|
||||||
|
|
||||||
|
from twisted.test.proto_helpers import MemoryReactor
|
||||||
|
|
||||||
|
from synapse.api.constants import EventTypes
|
||||||
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.config.auto_accept_invites import AutoAcceptInvitesConfig
|
||||||
|
from synapse.events.auto_accept_invites import InviteAutoAccepter
|
||||||
|
from synapse.federation.federation_base import event_from_pdu_json
|
||||||
|
from synapse.handlers.sync import JoinedSyncResult, SyncRequestKey, SyncVersion
|
||||||
|
from synapse.module_api import ModuleApi
|
||||||
|
from synapse.rest import admin
|
||||||
|
from synapse.rest.client import login, room
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.types import StreamToken, create_requester
|
||||||
|
from synapse.util import Clock
|
||||||
|
|
||||||
|
from tests.handlers.test_sync import generate_sync_config
|
||||||
|
from tests.unittest import (
|
||||||
|
FederatingHomeserverTestCase,
|
||||||
|
HomeserverTestCase,
|
||||||
|
TestCase,
|
||||||
|
override_config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AutoAcceptInvitesTestCase(FederatingHomeserverTestCase):
|
||||||
|
"""
|
||||||
|
Integration test cases for auto-accepting invites.
|
||||||
|
"""
|
||||||
|
|
||||||
|
servlets = [
|
||||||
|
admin.register_servlets,
|
||||||
|
login.register_servlets,
|
||||||
|
room.register_servlets,
|
||||||
|
]
|
||||||
|
|
||||||
|
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
|
||||||
|
hs = self.setup_test_homeserver()
|
||||||
|
self.handler = hs.get_federation_handler()
|
||||||
|
self.store = hs.get_datastores().main
|
||||||
|
return hs
|
||||||
|
|
||||||
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
||||||
|
self.sync_handler = self.hs.get_sync_handler()
|
||||||
|
self.module_api = hs.get_module_api()
|
||||||
|
|
||||||
|
@parameterized.expand(
|
||||||
|
[
|
||||||
|
[False],
|
||||||
|
[True],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"auto_accept_invites": {
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_auto_accept_invites(self, direct_room: bool) -> None:
|
||||||
|
"""Test that a user automatically joins a room when invited, if the
|
||||||
|
module is enabled.
|
||||||
|
"""
|
||||||
|
# A local user who sends an invite
|
||||||
|
inviting_user_id = self.register_user("inviter", "pass")
|
||||||
|
inviting_user_tok = self.login("inviter", "pass")
|
||||||
|
|
||||||
|
# A local user who receives an invite
|
||||||
|
invited_user_id = self.register_user("invitee", "pass")
|
||||||
|
self.login("invitee", "pass")
|
||||||
|
|
||||||
|
# Create a room and send an invite to the other user
|
||||||
|
room_id = self.helper.create_room_as(
|
||||||
|
inviting_user_id,
|
||||||
|
is_public=False,
|
||||||
|
tok=inviting_user_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.invite(
|
||||||
|
room_id,
|
||||||
|
inviting_user_id,
|
||||||
|
invited_user_id,
|
||||||
|
tok=inviting_user_tok,
|
||||||
|
extra_data={"is_direct": direct_room},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the invite receiving user has automatically joined the room when syncing
|
||||||
|
join_updates, _ = sync_join(self, invited_user_id)
|
||||||
|
self.assertEqual(len(join_updates), 1)
|
||||||
|
|
||||||
|
join_update: JoinedSyncResult = join_updates[0]
|
||||||
|
self.assertEqual(join_update.room_id, room_id)
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"auto_accept_invites": {
|
||||||
|
"enabled": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_module_not_enabled(self) -> None:
|
||||||
|
"""Test that a user does not automatically join a room when invited,
|
||||||
|
if the module is not enabled.
|
||||||
|
"""
|
||||||
|
# A local user who sends an invite
|
||||||
|
inviting_user_id = self.register_user("inviter", "pass")
|
||||||
|
inviting_user_tok = self.login("inviter", "pass")
|
||||||
|
|
||||||
|
# A local user who receives an invite
|
||||||
|
invited_user_id = self.register_user("invitee", "pass")
|
||||||
|
self.login("invitee", "pass")
|
||||||
|
|
||||||
|
# Create a room and send an invite to the other user
|
||||||
|
room_id = self.helper.create_room_as(
|
||||||
|
inviting_user_id, is_public=False, tok=inviting_user_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.invite(
|
||||||
|
room_id,
|
||||||
|
inviting_user_id,
|
||||||
|
invited_user_id,
|
||||||
|
tok=inviting_user_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the invite receiving user has not automatically joined the room when syncing
|
||||||
|
join_updates, _ = sync_join(self, invited_user_id)
|
||||||
|
self.assertEqual(len(join_updates), 0)
|
||||||
|
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"auto_accept_invites": {
|
||||||
|
"enabled": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_invite_from_remote_user(self) -> None:
|
||||||
|
"""Test that an invite from a remote user results in the invited user
|
||||||
|
automatically joining the room.
|
||||||
|
"""
|
||||||
|
# A remote user who sends the invite
|
||||||
|
remote_server = "otherserver"
|
||||||
|
remote_user = "@otheruser:" + remote_server
|
||||||
|
|
||||||
|
# A local user who creates the room
|
||||||
|
creator_user_id = self.register_user("creator", "pass")
|
||||||
|
creator_user_tok = self.login("creator", "pass")
|
||||||
|
|
||||||
|
# A local user who receives an invite
|
||||||
|
invited_user_id = self.register_user("invitee", "pass")
|
||||||
|
self.login("invitee", "pass")
|
||||||
|
|
||||||
|
room_id = self.helper.create_room_as(
|
||||||
|
room_creator=creator_user_id, tok=creator_user_tok
|
||||||
|
)
|
||||||
|
room_version = self.get_success(self.store.get_room_version(room_id))
|
||||||
|
|
||||||
|
invite_event = event_from_pdu_json(
|
||||||
|
{
|
||||||
|
"type": EventTypes.Member,
|
||||||
|
"content": {"membership": "invite"},
|
||||||
|
"room_id": room_id,
|
||||||
|
"sender": remote_user,
|
||||||
|
"state_key": invited_user_id,
|
||||||
|
"depth": 32,
|
||||||
|
"prev_events": [],
|
||||||
|
"auth_events": [],
|
||||||
|
"origin_server_ts": self.clock.time_msec(),
|
||||||
|
},
|
||||||
|
room_version,
|
||||||
|
)
|
||||||
|
self.get_success(
|
||||||
|
self.handler.on_invite_request(
|
||||||
|
remote_server,
|
||||||
|
invite_event,
|
||||||
|
invite_event.room_version,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the invite receiving user has automatically joined the room when syncing
|
||||||
|
join_updates, _ = sync_join(self, invited_user_id)
|
||||||
|
self.assertEqual(len(join_updates), 1)
|
||||||
|
|
||||||
|
join_update: JoinedSyncResult = join_updates[0]
|
||||||
|
self.assertEqual(join_update.room_id, room_id)
|
||||||
|
|
||||||
|
@parameterized.expand(
|
||||||
|
[
|
||||||
|
[False, False],
|
||||||
|
[True, True],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"auto_accept_invites": {
|
||||||
|
"enabled": True,
|
||||||
|
"only_for_direct_messages": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_accept_invite_direct_message(
|
||||||
|
self,
|
||||||
|
direct_room: bool,
|
||||||
|
expect_auto_join: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Tests that, if the module is configured to only accept DM invites, invites to DM rooms are still
|
||||||
|
automatically accepted. Otherwise they are rejected.
|
||||||
|
"""
|
||||||
|
# A local user who sends an invite
|
||||||
|
inviting_user_id = self.register_user("inviter", "pass")
|
||||||
|
inviting_user_tok = self.login("inviter", "pass")
|
||||||
|
|
||||||
|
# A local user who receives an invite
|
||||||
|
invited_user_id = self.register_user("invitee", "pass")
|
||||||
|
self.login("invitee", "pass")
|
||||||
|
|
||||||
|
# Create a room and send an invite to the other user
|
||||||
|
room_id = self.helper.create_room_as(
|
||||||
|
inviting_user_id,
|
||||||
|
is_public=False,
|
||||||
|
tok=inviting_user_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.helper.invite(
|
||||||
|
room_id,
|
||||||
|
inviting_user_id,
|
||||||
|
invited_user_id,
|
||||||
|
tok=inviting_user_tok,
|
||||||
|
extra_data={"is_direct": direct_room},
|
||||||
|
)
|
||||||
|
|
||||||
|
if expect_auto_join:
|
||||||
|
# Check that the invite receiving user has automatically joined the room when syncing
|
||||||
|
join_updates, _ = sync_join(self, invited_user_id)
|
||||||
|
self.assertEqual(len(join_updates), 1)
|
||||||
|
|
||||||
|
join_update: JoinedSyncResult = join_updates[0]
|
||||||
|
self.assertEqual(join_update.room_id, room_id)
|
||||||
|
else:
|
||||||
|
# Check that the invite receiving user has not automatically joined the room when syncing
|
||||||
|
join_updates, _ = sync_join(self, invited_user_id)
|
||||||
|
self.assertEqual(len(join_updates), 0)
|
||||||
|
|
||||||
|
@parameterized.expand(
|
||||||
|
[
|
||||||
|
[False, True],
|
||||||
|
[True, False],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@override_config(
|
||||||
|
{
|
||||||
|
"auto_accept_invites": {
|
||||||
|
"enabled": True,
|
||||||
|
"only_from_local_users": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def test_accept_invite_local_user(
|
||||||
|
self, remote_inviter: bool, expect_auto_join: bool
|
||||||
|
) -> None:
|
||||||
|
"""Tests that, if the module is configured to only accept invites from local users, invites
|
||||||
|
from local users are still automatically accepted. Otherwise they are rejected.
|
||||||
|
"""
|
||||||
|
# A local user who sends an invite
|
||||||
|
creator_user_id = self.register_user("inviter", "pass")
|
||||||
|
creator_user_tok = self.login("inviter", "pass")
|
||||||
|
|
||||||
|
# A local user who receives an invite
|
||||||
|
invited_user_id = self.register_user("invitee", "pass")
|
||||||
|
self.login("invitee", "pass")
|
||||||
|
|
||||||
|
# Create a room and send an invite to the other user
|
||||||
|
room_id = self.helper.create_room_as(
|
||||||
|
creator_user_id, is_public=False, tok=creator_user_tok
|
||||||
|
)
|
||||||
|
|
||||||
|
if remote_inviter:
|
||||||
|
room_version = self.get_success(self.store.get_room_version(room_id))
|
||||||
|
|
||||||
|
# A remote user who sends the invite
|
||||||
|
remote_server = "otherserver"
|
||||||
|
remote_user = "@otheruser:" + remote_server
|
||||||
|
|
||||||
|
invite_event = event_from_pdu_json(
|
||||||
|
{
|
||||||
|
"type": EventTypes.Member,
|
||||||
|
"content": {"membership": "invite"},
|
||||||
|
"room_id": room_id,
|
||||||
|
"sender": remote_user,
|
||||||
|
"state_key": invited_user_id,
|
||||||
|
"depth": 32,
|
||||||
|
"prev_events": [],
|
||||||
|
"auth_events": [],
|
||||||
|
"origin_server_ts": self.clock.time_msec(),
|
||||||
|
},
|
||||||
|
room_version,
|
||||||
|
)
|
||||||
|
self.get_success(
|
||||||
|
self.handler.on_invite_request(
|
||||||
|
remote_server,
|
||||||
|
invite_event,
|
||||||
|
invite_event.room_version,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.helper.invite(
|
||||||
|
room_id,
|
||||||
|
creator_user_id,
|
||||||
|
invited_user_id,
|
||||||
|
tok=creator_user_tok,
|
||||||
|
)
|
||||||
|
|
||||||
|
if expect_auto_join:
|
||||||
|
# Check that the invite receiving user has automatically joined the room when syncing
|
||||||
|
join_updates, _ = sync_join(self, invited_user_id)
|
||||||
|
self.assertEqual(len(join_updates), 1)
|
||||||
|
|
||||||
|
join_update: JoinedSyncResult = join_updates[0]
|
||||||
|
self.assertEqual(join_update.room_id, room_id)
|
||||||
|
else:
|
||||||
|
# Check that the invite receiving user has not automatically joined the room when syncing
|
||||||
|
join_updates, _ = sync_join(self, invited_user_id)
|
||||||
|
self.assertEqual(len(join_updates), 0)
|
||||||
|
|
||||||
|
|
||||||
|
_request_key = 0
|
||||||
|
|
||||||
|
|
||||||
|
def generate_request_key() -> SyncRequestKey:
|
||||||
|
global _request_key
|
||||||
|
_request_key += 1
|
||||||
|
return ("request_key", _request_key)
|
||||||
|
|
||||||
|
|
||||||
|
def sync_join(
|
||||||
|
testcase: HomeserverTestCase,
|
||||||
|
user_id: str,
|
||||||
|
since_token: Optional[StreamToken] = None,
|
||||||
|
) -> Tuple[List[JoinedSyncResult], StreamToken]:
|
||||||
|
"""Perform a sync request for the given user and return the user join updates
|
||||||
|
they've received, as well as the next_batch token.
|
||||||
|
|
||||||
|
This method assumes testcase.sync_handler points to the homeserver's sync handler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
testcase: The testcase that is currently being run.
|
||||||
|
user_id: The ID of the user to generate a sync response for.
|
||||||
|
since_token: An optional token to indicate from at what point to sync from.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tuple containing a list of join updates, and the sync response's
|
||||||
|
next_batch token.
|
||||||
|
"""
|
||||||
|
requester = create_requester(user_id)
|
||||||
|
sync_config = generate_sync_config(requester.user.to_string())
|
||||||
|
sync_result = testcase.get_success(
|
||||||
|
testcase.hs.get_sync_handler().wait_for_sync_for_user(
|
||||||
|
requester,
|
||||||
|
sync_config,
|
||||||
|
SyncVersion.SYNC_V2,
|
||||||
|
generate_request_key(),
|
||||||
|
since_token,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return sync_result.joined, sync_result.next_batch
|
||||||
|
|
||||||
|
|
||||||
|
class InviteAutoAccepterInternalTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
Test cases which exercise the internals of the InviteAutoAccepter.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.module = create_module()
|
||||||
|
self.user_id = "@peter:test"
|
||||||
|
self.invitee = "@lesley:test"
|
||||||
|
self.remote_invitee = "@thomas:remote"
|
||||||
|
|
||||||
|
# We know our module API is a mock, but mypy doesn't.
|
||||||
|
self.mocked_update_membership: Mock = self.module._api.update_room_membership # type: ignore[assignment]
|
||||||
|
|
||||||
|
async def test_accept_invite_with_failures(self) -> None:
|
||||||
|
"""Tests that receiving an invite for a local user makes the module attempt to
|
||||||
|
make the invitee join the room. This test verifies that it works if the call to
|
||||||
|
update membership returns exceptions before successfully completing and returning an event.
|
||||||
|
"""
|
||||||
|
invite = MockEvent(
|
||||||
|
sender="@inviter:test",
|
||||||
|
state_key="@invitee:test",
|
||||||
|
type="m.room.member",
|
||||||
|
content={"membership": "invite"},
|
||||||
|
)
|
||||||
|
|
||||||
|
join_event = MockEvent(
|
||||||
|
sender="someone",
|
||||||
|
state_key="someone",
|
||||||
|
type="m.room.member",
|
||||||
|
content={"membership": "join"},
|
||||||
|
)
|
||||||
|
# the first two calls raise an exception while the third call is successful
|
||||||
|
self.mocked_update_membership.side_effect = [
|
||||||
|
SynapseError(HTTPStatus.FORBIDDEN, "Forbidden"),
|
||||||
|
SynapseError(HTTPStatus.FORBIDDEN, "Forbidden"),
|
||||||
|
make_awaitable(join_event),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Stop mypy from complaining that we give on_new_event a MockEvent rather than an
|
||||||
|
# EventBase.
|
||||||
|
await self.module.on_new_event(event=invite) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
await self.retry_assertions(
|
||||||
|
self.mocked_update_membership,
|
||||||
|
3,
|
||||||
|
sender=invite.state_key,
|
||||||
|
target=invite.state_key,
|
||||||
|
room_id=invite.room_id,
|
||||||
|
new_membership="join",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_accept_invite_failures(self) -> None:
|
||||||
|
"""Tests that receiving an invite for a local user makes the module attempt to
|
||||||
|
make the invitee join the room. This test verifies that if the update_membership call
|
||||||
|
fails consistently, _retry_make_join will break the loop after the set number of retries and
|
||||||
|
execution will continue.
|
||||||
|
"""
|
||||||
|
invite = MockEvent(
|
||||||
|
sender=self.user_id,
|
||||||
|
state_key=self.invitee,
|
||||||
|
type="m.room.member",
|
||||||
|
content={"membership": "invite"},
|
||||||
|
)
|
||||||
|
self.mocked_update_membership.side_effect = SynapseError(
|
||||||
|
HTTPStatus.FORBIDDEN, "Forbidden"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stop mypy from complaining that we give on_new_event a MockEvent rather than an
|
||||||
|
# EventBase.
|
||||||
|
await self.module.on_new_event(event=invite) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
await self.retry_assertions(
|
||||||
|
self.mocked_update_membership,
|
||||||
|
5,
|
||||||
|
sender=invite.state_key,
|
||||||
|
target=invite.state_key,
|
||||||
|
room_id=invite.room_id,
|
||||||
|
new_membership="join",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def test_not_state(self) -> None:
|
||||||
|
"""Tests that receiving an invite that's not a state event does nothing."""
|
||||||
|
invite = MockEvent(
|
||||||
|
sender=self.user_id, type="m.room.member", content={"membership": "invite"}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stop mypy from complaining that we give on_new_event a MockEvent rather than an
|
||||||
|
# EventBase.
|
||||||
|
await self.module.on_new_event(event=invite) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
self.mocked_update_membership.assert_not_called()
|
||||||
|
|
||||||
|
async def test_not_invite(self) -> None:
|
||||||
|
"""Tests that receiving a membership update that's not an invite does nothing."""
|
||||||
|
invite = MockEvent(
|
||||||
|
sender=self.user_id,
|
||||||
|
state_key=self.user_id,
|
||||||
|
type="m.room.member",
|
||||||
|
content={"membership": "join"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stop mypy from complaining that we give on_new_event a MockEvent rather than an
|
||||||
|
# EventBase.
|
||||||
|
await self.module.on_new_event(event=invite) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
self.mocked_update_membership.assert_not_called()
|
||||||
|
|
||||||
|
async def test_not_membership(self) -> None:
|
||||||
|
"""Tests that receiving a state event that's not a membership update does
|
||||||
|
nothing.
|
||||||
|
"""
|
||||||
|
invite = MockEvent(
|
||||||
|
sender=self.user_id,
|
||||||
|
state_key=self.user_id,
|
||||||
|
type="org.matrix.test",
|
||||||
|
content={"foo": "bar"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stop mypy from complaining that we give on_new_event a MockEvent rather than an
|
||||||
|
# EventBase.
|
||||||
|
await self.module.on_new_event(event=invite) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
self.mocked_update_membership.assert_not_called()
|
||||||
|
|
||||||
|
def test_config_parse(self) -> None:
|
||||||
|
"""Tests that a correct configuration parses."""
|
||||||
|
config = {
|
||||||
|
"auto_accept_invites": {
|
||||||
|
"enabled": True,
|
||||||
|
"only_for_direct_messages": True,
|
||||||
|
"only_from_local_users": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsed_config = AutoAcceptInvitesConfig()
|
||||||
|
parsed_config.read_config(config)
|
||||||
|
|
||||||
|
self.assertTrue(parsed_config.enabled)
|
||||||
|
self.assertTrue(parsed_config.accept_invites_only_for_direct_messages)
|
||||||
|
self.assertTrue(parsed_config.accept_invites_only_from_local_users)
|
||||||
|
|
||||||
|
def test_runs_on_only_one_worker(self) -> None:
|
||||||
|
"""
|
||||||
|
Tests that the module only runs on the specified worker.
|
||||||
|
"""
|
||||||
|
# By default, we run on the main process...
|
||||||
|
main_module = create_module(
|
||||||
|
config_override={"auto_accept_invites": {"enabled": True}}, worker_name=None
|
||||||
|
)
|
||||||
|
cast(
|
||||||
|
Mock, main_module._api.register_third_party_rules_callbacks
|
||||||
|
).assert_called_once()
|
||||||
|
|
||||||
|
# ...and not on other workers (like synchrotrons)...
|
||||||
|
sync_module = create_module(worker_name="synchrotron42")
|
||||||
|
cast(
|
||||||
|
Mock, sync_module._api.register_third_party_rules_callbacks
|
||||||
|
).assert_not_called()
|
||||||
|
|
||||||
|
# ...unless we configured them to be the designated worker.
|
||||||
|
specified_module = create_module(
|
||||||
|
config_override={
|
||||||
|
"auto_accept_invites": {
|
||||||
|
"enabled": True,
|
||||||
|
"worker_to_run_on": "account_data1",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
worker_name="account_data1",
|
||||||
|
)
|
||||||
|
cast(
|
||||||
|
Mock, specified_module._api.register_third_party_rules_callbacks
|
||||||
|
).assert_called_once()
|
||||||
|
|
||||||
|
async def retry_assertions(
|
||||||
|
self, mock: Mock, call_count: int, **kwargs: Any
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
This is a hacky way to ensure that the assertions are not called before the other coroutine
|
||||||
|
has a chance to call `update_room_membership`. It catches the exception caused by a failure,
|
||||||
|
and sleeps the thread before retrying, up until 5 tries.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
call_count: the number of times the mock should have been called
|
||||||
|
mock: the mocked function we want to assert on
|
||||||
|
kwargs: keyword arguments to assert that the mock was called with
|
||||||
|
"""
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < 5:
|
||||||
|
try:
|
||||||
|
# Check that the mocked method is called the expected amount of times and with the right
|
||||||
|
# arguments to attempt to make the user join the room.
|
||||||
|
mock.assert_called_with(**kwargs)
|
||||||
|
self.assertEqual(call_count, mock.call_count)
|
||||||
|
break
|
||||||
|
except AssertionError as e:
|
||||||
|
i += 1
|
||||||
|
if i == 5:
|
||||||
|
# we've used up the tries, force the test to fail as we've already caught the exception
|
||||||
|
self.fail(e)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s(auto_attribs=True)
|
||||||
|
class MockEvent:
|
||||||
|
"""Mocks an event. Only exposes properties the module uses."""
|
||||||
|
|
||||||
|
sender: str
|
||||||
|
type: str
|
||||||
|
content: Dict[str, Any]
|
||||||
|
room_id: str = "!someroom"
|
||||||
|
state_key: Optional[str] = None
|
||||||
|
|
||||||
|
def is_state(self) -> bool:
|
||||||
|
"""Checks if the event is a state event by checking if it has a state key."""
|
||||||
|
return self.state_key is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def membership(self) -> str:
|
||||||
|
"""Extracts the membership from the event. Should only be called on an event
|
||||||
|
that's a membership event, and will raise a KeyError otherwise.
|
||||||
|
"""
|
||||||
|
membership: str = self.content["membership"]
|
||||||
|
return membership
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
TV = TypeVar("TV")
|
||||||
|
|
||||||
|
|
||||||
|
async def make_awaitable(value: T) -> T:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def make_multiple_awaitable(result: TV) -> Awaitable[TV]:
|
||||||
|
"""
|
||||||
|
Makes an awaitable, suitable for mocking an `async` function.
|
||||||
|
This uses Futures as they can be awaited multiple times so can be returned
|
||||||
|
to multiple callers.
|
||||||
|
"""
|
||||||
|
future: Future[TV] = Future()
|
||||||
|
future.set_result(result)
|
||||||
|
return future
|
||||||
|
|
||||||
|
|
||||||
|
def create_module(
|
||||||
|
config_override: Optional[Dict[str, Any]] = None, worker_name: Optional[str] = None
|
||||||
|
) -> InviteAutoAccepter:
|
||||||
|
# Create a mock based on the ModuleApi spec, but override some mocked functions
|
||||||
|
# because some capabilities are needed for running the tests.
|
||||||
|
module_api = Mock(spec=ModuleApi)
|
||||||
|
module_api.is_mine.side_effect = lambda a: a.split(":")[1] == "test"
|
||||||
|
module_api.worker_name = worker_name
|
||||||
|
module_api.sleep.return_value = make_multiple_awaitable(None)
|
||||||
|
|
||||||
|
if config_override is None:
|
||||||
|
config_override = {}
|
||||||
|
|
||||||
|
config = AutoAcceptInvitesConfig()
|
||||||
|
config.read_config(config_override)
|
||||||
|
|
||||||
|
return InviteAutoAccepter(config, module_api)
|
|
@ -170,6 +170,7 @@ class RestHelper:
|
||||||
targ: Optional[str] = None,
|
targ: Optional[str] = None,
|
||||||
expect_code: int = HTTPStatus.OK,
|
expect_code: int = HTTPStatus.OK,
|
||||||
tok: Optional[str] = None,
|
tok: Optional[str] = None,
|
||||||
|
extra_data: Optional[dict] = None,
|
||||||
) -> JsonDict:
|
) -> JsonDict:
|
||||||
return self.change_membership(
|
return self.change_membership(
|
||||||
room=room,
|
room=room,
|
||||||
|
@ -178,6 +179,7 @@ class RestHelper:
|
||||||
tok=tok,
|
tok=tok,
|
||||||
membership=Membership.INVITE,
|
membership=Membership.INVITE,
|
||||||
expect_code=expect_code,
|
expect_code=expect_code,
|
||||||
|
extra_data=extra_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
def join(
|
def join(
|
||||||
|
|
|
@ -85,6 +85,7 @@ from twisted.web.server import Request, Site
|
||||||
|
|
||||||
from synapse.config.database import DatabaseConnectionConfig
|
from synapse.config.database import DatabaseConnectionConfig
|
||||||
from synapse.config.homeserver import HomeServerConfig
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
|
from synapse.events.auto_accept_invites import InviteAutoAccepter
|
||||||
from synapse.events.presence_router import load_legacy_presence_router
|
from synapse.events.presence_router import load_legacy_presence_router
|
||||||
from synapse.handlers.auth import load_legacy_password_auth_providers
|
from synapse.handlers.auth import load_legacy_password_auth_providers
|
||||||
from synapse.http.site import SynapseRequest
|
from synapse.http.site import SynapseRequest
|
||||||
|
@ -1156,6 +1157,11 @@ def setup_test_homeserver(
|
||||||
for module, module_config in hs.config.modules.loaded_modules:
|
for module, module_config in hs.config.modules.loaded_modules:
|
||||||
module(config=module_config, api=module_api)
|
module(config=module_config, api=module_api)
|
||||||
|
|
||||||
|
if hs.config.auto_accept_invites.enabled:
|
||||||
|
# Start the local auto_accept_invites module.
|
||||||
|
m = InviteAutoAccepter(hs.config.auto_accept_invites, module_api)
|
||||||
|
logger.info("Loaded local module %s", m)
|
||||||
|
|
||||||
load_legacy_spam_checkers(hs)
|
load_legacy_spam_checkers(hs)
|
||||||
load_legacy_third_party_event_rules(hs)
|
load_legacy_third_party_event_rules(hs)
|
||||||
load_legacy_presence_router(hs)
|
load_legacy_presence_router(hs)
|
||||||
|
|
Loading…
Add table
Reference in a new issue