summary refs log tree commit diff
path: root/synapse
diff options
context:
space:
mode:
authorAndrew Morgan <andrew@amorgan.xyz>2023-03-08 18:00:58 +0000
committerAndrew Morgan <andrew@amorgan.xyz>2023-03-09 16:50:31 +0000
commitc3c3c6d2005b70ef49526f9a4b99c39feebf3775 (patch)
tree7b710409a85db92ac2fd3cb0fca56c39c2da243b /synapse
parentMove Account Validity callbacks to a dedicated file (diff)
downloadsynapse-c3c3c6d2005b70ef49526f9a4b99c39feebf3775.tar.xz
Move callback-related code from the SpamChecker to its own class
And update the many references.
Diffstat (limited to 'synapse')
-rw-r--r--synapse/app/_base.py4
-rw-r--r--synapse/events/spamcheck.py370
-rw-r--r--synapse/module_api/__init__.py31
-rw-r--r--synapse/module_api/callbacks/__init__.py2
-rw-r--r--synapse/module_api/callbacks/spam_checker_callbacks.py373
5 files changed, 412 insertions, 368 deletions
diff --git a/synapse/app/_base.py b/synapse/app/_base.py

index 28062dd69d..1795d10917 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py
@@ -59,7 +59,6 @@ from synapse.config.homeserver import HomeServerConfig from synapse.config.server import ListenerConfig, ManholeConfig from synapse.crypto import context_factory from synapse.events.presence_router import load_legacy_presence_router -from synapse.events.spamcheck import load_legacy_spam_checkers from synapse.events.third_party_rules import load_legacy_third_party_event_rules from synapse.handlers.auth import load_legacy_password_auth_providers from synapse.http.site import SynapseSite @@ -68,6 +67,9 @@ from synapse.logging.opentracing import init_tracer from synapse.metrics import install_gc_manager, register_threadpool from synapse.metrics.background_process_metrics import wrap_as_background_process from synapse.metrics.jemalloc import setup_jemalloc_stats +from synapse.module_api.callbacks.spam_checker_callbacks import ( + load_legacy_spam_checkers, +) from synapse.types import ISynapseReactor from synapse.util import SYNAPSE_VERSION from synapse.util.caches.lrucache import setup_expire_lru_cache_entries diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py
index 765c15bb51..c4becbfd5b 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py
@@ -13,19 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import inspect import logging -from typing import ( - TYPE_CHECKING, - Any, - Awaitable, - Callable, - Collection, - List, - Optional, - Tuple, - Union, -) +from typing import TYPE_CHECKING, Collection, Optional, Tuple, Union # `Literal` appears with Python 3.8. from typing_extensions import Literal @@ -37,7 +26,7 @@ from synapse.media._base import FileInfo from synapse.media.media_storage import ReadableFileWrapper from synapse.spam_checker_api import RegistrationBehaviour from synapse.types import JsonDict, RoomAlias, UserProfile -from synapse.util.async_helpers import delay_cancellation, maybe_awaitable +from synapse.util.async_helpers import delay_cancellation from synapse.util.metrics import Measure if TYPE_CHECKING: @@ -46,338 +35,13 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -CHECK_EVENT_FOR_SPAM_CALLBACK = Callable[ - ["synapse.events.EventBase"], - Awaitable[ - Union[ - str, - Codes, - # Highly experimental, not officially part of the spamchecker API, may - # disappear without warning depending on the results of ongoing - # experiments. - # Use this to return additional information as part of an error. - Tuple[Codes, JsonDict], - # Deprecated - bool, - ] - ], -] -SHOULD_DROP_FEDERATED_EVENT_CALLBACK = Callable[ - ["synapse.events.EventBase"], - Awaitable[Union[bool, str]], -] -USER_MAY_JOIN_ROOM_CALLBACK = Callable[ - [str, str, bool], - Awaitable[ - Union[ - Literal["NOT_SPAM"], - Codes, - # Highly experimental, not officially part of the spamchecker API, may - # disappear without warning depending on the results of ongoing - # experiments. - # Use this to return additional information as part of an error. - Tuple[Codes, JsonDict], - # Deprecated - bool, - ] - ], -] -USER_MAY_INVITE_CALLBACK = Callable[ - [str, str, str], - Awaitable[ - Union[ - Literal["NOT_SPAM"], - Codes, - # Highly experimental, not officially part of the spamchecker API, may - # disappear without warning depending on the results of ongoing - # experiments. - # Use this to return additional information as part of an error. - Tuple[Codes, JsonDict], - # Deprecated - bool, - ] - ], -] -USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[ - [str, str, str, str], - Awaitable[ - Union[ - Literal["NOT_SPAM"], - Codes, - # Highly experimental, not officially part of the spamchecker API, may - # disappear without warning depending on the results of ongoing - # experiments. - # Use this to return additional information as part of an error. - Tuple[Codes, JsonDict], - # Deprecated - bool, - ] - ], -] -USER_MAY_CREATE_ROOM_CALLBACK = Callable[ - [str], - Awaitable[ - Union[ - Literal["NOT_SPAM"], - Codes, - # Highly experimental, not officially part of the spamchecker API, may - # disappear without warning depending on the results of ongoing - # experiments. - # Use this to return additional information as part of an error. - Tuple[Codes, JsonDict], - # Deprecated - bool, - ] - ], -] -USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[ - [str, RoomAlias], - Awaitable[ - Union[ - Literal["NOT_SPAM"], - Codes, - # Highly experimental, not officially part of the spamchecker API, may - # disappear without warning depending on the results of ongoing - # experiments. - # Use this to return additional information as part of an error. - Tuple[Codes, JsonDict], - # Deprecated - bool, - ] - ], -] -USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[ - [str, str], - Awaitable[ - Union[ - Literal["NOT_SPAM"], - Codes, - # Highly experimental, not officially part of the spamchecker API, may - # disappear without warning depending on the results of ongoing - # experiments. - # Use this to return additional information as part of an error. - Tuple[Codes, JsonDict], - # Deprecated - bool, - ] - ], -] -CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[UserProfile], Awaitable[bool]] -LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[ - [ - Optional[dict], - Optional[str], - Collection[Tuple[str, str]], - ], - Awaitable[RegistrationBehaviour], -] -CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[ - [ - Optional[dict], - Optional[str], - Collection[Tuple[str, str]], - Optional[str], - ], - Awaitable[RegistrationBehaviour], -] -CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK = Callable[ - [ReadableFileWrapper, FileInfo], - Awaitable[ - Union[ - Literal["NOT_SPAM"], - Codes, - # Highly experimental, not officially part of the spamchecker API, may - # disappear without warning depending on the results of ongoing - # experiments. - # Use this to return additional information as part of an error. - Tuple[Codes, JsonDict], - # Deprecated - bool, - ] - ], -] - - -def load_legacy_spam_checkers(hs: "synapse.server.HomeServer") -> None: - """Wrapper that loads spam checkers configured using the old configuration, and - registers the spam checker hooks they implement. - """ - spam_checkers: List[Any] = [] - api = hs.get_module_api() - for module, config in hs.config.spamchecker.spam_checkers: - # Older spam checkers don't accept the `api` argument, so we - # try and detect support. - spam_args = inspect.getfullargspec(module) - if "api" in spam_args.args: - spam_checkers.append(module(config=config, api=api)) - else: - spam_checkers.append(module(config=config)) - - # The known spam checker hooks. If a spam checker module implements a method - # which name appears in this set, we'll want to register it. - spam_checker_methods = { - "check_event_for_spam", - "user_may_invite", - "user_may_create_room", - "user_may_create_room_alias", - "user_may_publish_room", - "check_username_for_spam", - "check_registration_for_spam", - "check_media_file_for_spam", - } - - for spam_checker in spam_checkers: - # Methods on legacy spam checkers might not be async, so we wrap them around a - # wrapper that will call maybe_awaitable on the result. - def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]: - # f might be None if the callback isn't implemented by the module. In this - # case we don't want to register a callback at all so we return None. - if f is None: - return None - - wrapped_func = f - - if f.__name__ == "check_registration_for_spam": - checker_args = inspect.signature(f) - if len(checker_args.parameters) == 3: - # Backwards compatibility; some modules might implement a hook that - # doesn't expect a 4th argument. In this case, wrap it in a function - # that gives it only 3 arguments and drops the auth_provider_id on - # the floor. - def wrapper( - email_threepid: Optional[dict], - username: Optional[str], - request_info: Collection[Tuple[str, str]], - auth_provider_id: Optional[str], - ) -> Union[Awaitable[RegistrationBehaviour], RegistrationBehaviour]: - # Assertion required because mypy can't prove we won't - # change `f` back to `None`. See - # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions - assert f is not None - - return f( - email_threepid, - username, - request_info, - ) - - wrapped_func = wrapper - elif len(checker_args.parameters) != 4: - raise RuntimeError( - "Bad signature for callback check_registration_for_spam", - ) - - def run(*args: Any, **kwargs: Any) -> Awaitable: - # Assertion required because mypy can't prove we won't change `f` - # back to `None`. See - # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions - assert wrapped_func is not None - - return maybe_awaitable(wrapped_func(*args, **kwargs)) - - return run - - # Register the hooks through the module API. - hooks = { - hook: async_wrapper(getattr(spam_checker, hook, None)) - for hook in spam_checker_methods - } - - api.register_spam_checker_callbacks(**hooks) - class SpamChecker: NOT_SPAM: Literal["NOT_SPAM"] = "NOT_SPAM" - def __init__(self, hs: "synapse.server.HomeServer") -> None: - self.hs = hs + def __init__(self, hs: "synapse.server.HomeServer"): self.clock = hs.get_clock() - - self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = [] - self._should_drop_federated_event_callbacks: List[ - SHOULD_DROP_FEDERATED_EVENT_CALLBACK - ] = [] - self._user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = [] - self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = [] - self._user_may_send_3pid_invite_callbacks: List[ - USER_MAY_SEND_3PID_INVITE_CALLBACK - ] = [] - self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = [] - self._user_may_create_room_alias_callbacks: List[ - USER_MAY_CREATE_ROOM_ALIAS_CALLBACK - ] = [] - self._user_may_publish_room_callbacks: List[USER_MAY_PUBLISH_ROOM_CALLBACK] = [] - self._check_username_for_spam_callbacks: List[ - CHECK_USERNAME_FOR_SPAM_CALLBACK - ] = [] - self._check_registration_for_spam_callbacks: List[ - CHECK_REGISTRATION_FOR_SPAM_CALLBACK - ] = [] - self._check_media_file_for_spam_callbacks: List[ - CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK - ] = [] - - def register_callbacks( - self, - check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None, - should_drop_federated_event: Optional[ - SHOULD_DROP_FEDERATED_EVENT_CALLBACK - ] = None, - user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None, - user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None, - user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None, - user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None, - user_may_create_room_alias: Optional[ - USER_MAY_CREATE_ROOM_ALIAS_CALLBACK - ] = None, - user_may_publish_room: Optional[USER_MAY_PUBLISH_ROOM_CALLBACK] = None, - check_username_for_spam: Optional[CHECK_USERNAME_FOR_SPAM_CALLBACK] = None, - check_registration_for_spam: Optional[ - CHECK_REGISTRATION_FOR_SPAM_CALLBACK - ] = None, - check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None, - ) -> None: - """Register callbacks from module for each hook.""" - if check_event_for_spam is not None: - self._check_event_for_spam_callbacks.append(check_event_for_spam) - - if should_drop_federated_event is not None: - self._should_drop_federated_event_callbacks.append( - should_drop_federated_event - ) - - if user_may_join_room is not None: - self._user_may_join_room_callbacks.append(user_may_join_room) - - if user_may_invite is not None: - self._user_may_invite_callbacks.append(user_may_invite) - - if user_may_send_3pid_invite is not None: - self._user_may_send_3pid_invite_callbacks.append( - user_may_send_3pid_invite, - ) - - if user_may_create_room is not None: - self._user_may_create_room_callbacks.append(user_may_create_room) - - if user_may_create_room_alias is not None: - self._user_may_create_room_alias_callbacks.append( - user_may_create_room_alias, - ) - - if user_may_publish_room is not None: - self._user_may_publish_room_callbacks.append(user_may_publish_room) - - if check_username_for_spam is not None: - self._check_username_for_spam_callbacks.append(check_username_for_spam) - - if check_registration_for_spam is not None: - self._check_registration_for_spam_callbacks.append( - check_registration_for_spam, - ) - - if check_media_file_for_spam is not None: - self._check_media_file_for_spam_callbacks.append(check_media_file_for_spam) + self._module_api_callbacks = hs.get_module_api_callbacks().spam_checker @trace async def check_event_for_spam( @@ -401,7 +65,7 @@ class SpamChecker: string should be used as the client-facing error message. This usage is generally discouraged as it doesn't support internationalization. """ - for callback in self._check_event_for_spam_callbacks: + for callback in self._module_api_callbacks.check_event_for_spam_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): @@ -456,7 +120,9 @@ class SpamChecker: Returns: True if the event should be silently dropped """ - for callback in self._should_drop_federated_event_callbacks: + for ( + callback + ) in self._module_api_callbacks.should_drop_federated_event_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): @@ -480,7 +146,7 @@ class SpamChecker: Returns: NOT_SPAM if the operation is permitted, [Codes, Dict] otherwise. """ - for callback in self._user_may_join_room_callbacks: + for callback in self._module_api_callbacks.user_may_join_room_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): @@ -521,7 +187,7 @@ class SpamChecker: Returns: NOT_SPAM if the operation is permitted, Codes otherwise. """ - for callback in self._user_may_invite_callbacks: + for callback in self._module_api_callbacks.user_may_invite_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): @@ -568,7 +234,7 @@ class SpamChecker: Returns: NOT_SPAM if the operation is permitted, Codes otherwise. """ - for callback in self._user_may_send_3pid_invite_callbacks: + for callback in self._module_api_callbacks.user_may_send_3pid_invite_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): @@ -605,7 +271,7 @@ class SpamChecker: Args: userid: The ID of the user attempting to create a room """ - for callback in self._user_may_create_room_callbacks: + for callback in self._module_api_callbacks.user_may_create_room_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): @@ -641,7 +307,7 @@ class SpamChecker: room_alias: The alias to be created """ - for callback in self._user_may_create_room_alias_callbacks: + for callback in self._module_api_callbacks.user_may_create_room_alias_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): @@ -676,7 +342,7 @@ class SpamChecker: userid: The user ID attempting to publish the room room_id: The ID of the room that would be published """ - for callback in self._user_may_publish_room_callbacks: + for callback in self._module_api_callbacks.user_may_publish_room_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): @@ -717,7 +383,7 @@ class SpamChecker: Returns: True if the user is spammy. """ - for callback in self._check_username_for_spam_callbacks: + for callback in self._module_api_callbacks.check_username_for_spam_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): @@ -751,7 +417,9 @@ class SpamChecker: Enum for how the request should be handled """ - for callback in self._check_registration_for_spam_callbacks: + for ( + callback + ) in self._module_api_callbacks.check_registration_for_spam_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): @@ -794,7 +462,7 @@ class SpamChecker: file_info: Metadata about the file. """ - for callback in self._check_media_file_for_spam_callbacks: + for callback in self._module_api_callbacks.check_media_file_for_spam_callbacks: with Measure( self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) ): diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py
index 595c23e78d..bc272c83f5 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py
@@ -44,20 +44,7 @@ from synapse.events.presence_router import ( GET_USERS_FOR_STATES_CALLBACK, PresenceRouter, ) -from synapse.events.spamcheck import ( - CHECK_EVENT_FOR_SPAM_CALLBACK, - CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK, - CHECK_REGISTRATION_FOR_SPAM_CALLBACK, - CHECK_USERNAME_FOR_SPAM_CALLBACK, - SHOULD_DROP_FEDERATED_EVENT_CALLBACK, - USER_MAY_CREATE_ROOM_ALIAS_CALLBACK, - USER_MAY_CREATE_ROOM_CALLBACK, - USER_MAY_INVITE_CALLBACK, - USER_MAY_JOIN_ROOM_CALLBACK, - USER_MAY_PUBLISH_ROOM_CALLBACK, - USER_MAY_SEND_3PID_INVITE_CALLBACK, - SpamChecker, -) +from synapse.events.spamcheck import SpamChecker from synapse.events.third_party_rules import ( CHECK_CAN_DEACTIVATE_USER_CALLBACK, CHECK_CAN_SHUTDOWN_ROOM_CALLBACK, @@ -105,6 +92,19 @@ from synapse.module_api.callbacks.account_validity_callbacks import ( ON_LEGACY_SEND_MAIL_CALLBACK, ON_USER_REGISTRATION_CALLBACK, ) +from synapse.module_api.callbacks.spam_checker_callbacks import ( + CHECK_EVENT_FOR_SPAM_CALLBACK, + CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK, + CHECK_REGISTRATION_FOR_SPAM_CALLBACK, + CHECK_USERNAME_FOR_SPAM_CALLBACK, + SHOULD_DROP_FEDERATED_EVENT_CALLBACK, + USER_MAY_CREATE_ROOM_ALIAS_CALLBACK, + USER_MAY_CREATE_ROOM_CALLBACK, + USER_MAY_INVITE_CALLBACK, + USER_MAY_JOIN_ROOM_CALLBACK, + USER_MAY_PUBLISH_ROOM_CALLBACK, + USER_MAY_SEND_3PID_INVITE_CALLBACK, +) from synapse.rest.client.login import LoginResponse from synapse.storage import DataStore from synapse.storage.background_updates import ( @@ -271,7 +271,6 @@ class ModuleApi: self._public_room_list_manager = PublicRoomListManager(hs) self._account_data_manager = AccountDataManager(hs) - self._spam_checker = hs.get_spam_checker() self._third_party_event_rules = hs.get_third_party_event_rules() self._password_auth_provider = hs.get_password_auth_provider() self._presence_router = hs.get_presence_router() @@ -305,7 +304,7 @@ class ModuleApi: Added in Synapse v1.37.0. """ - return self._spam_checker.register_callbacks( + return self._callbacks.spam_checker.register_callbacks( check_event_for_spam=check_event_for_spam, should_drop_federated_event=should_drop_federated_event, user_may_join_room=user_may_join_room, diff --git a/synapse/module_api/callbacks/__init__.py b/synapse/module_api/callbacks/__init__.py
index 884c6413cc..d4c47a7c62 100644 --- a/synapse/module_api/callbacks/__init__.py +++ b/synapse/module_api/callbacks/__init__.py
@@ -13,6 +13,7 @@ # limitations under the License. from .account_validity_callbacks import AccountValidityModuleApiCallbacks +from .spam_checker_callbacks import SpamCheckerModuleApiCallbacks __all__ = [ "ModuleApiCallbacks", @@ -22,3 +23,4 @@ __all__ = [ class ModuleApiCallbacks: def __init__(self) -> None: self.account_validity = AccountValidityModuleApiCallbacks() + self.spam_checker = SpamCheckerModuleApiCallbacks() diff --git a/synapse/module_api/callbacks/spam_checker_callbacks.py b/synapse/module_api/callbacks/spam_checker_callbacks.py new file mode 100644
index 0000000000..266276e45e --- /dev/null +++ b/synapse/module_api/callbacks/spam_checker_callbacks.py
@@ -0,0 +1,373 @@ +# Copyright 2017 New Vector Ltd +# Copyright 2019, 2023 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. +import inspect +import logging +from typing import ( + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Collection, + List, + Optional, + Tuple, + Union, +) + +# `Literal` appears with Python 3.8. +from typing_extensions import Literal + +import synapse +from synapse.api.errors import Codes +from synapse.rest.media.v1._base import FileInfo +from synapse.rest.media.v1.media_storage import ReadableFileWrapper +from synapse.spam_checker_api import RegistrationBehaviour +from synapse.types import JsonDict, RoomAlias, UserProfile +from synapse.util.async_helpers import maybe_awaitable + +if TYPE_CHECKING: + import synapse.events + import synapse.server + +logger = logging.getLogger(__name__) + + +CHECK_EVENT_FOR_SPAM_CALLBACK = Callable[ + ["synapse.events.EventBase"], + Awaitable[ + Union[ + str, + Codes, + # Highly experimental, not officially part of the spamchecker API, may + # disappear without warning depending on the results of ongoing + # experiments. + # Use this to return additional information as part of an error. + Tuple[Codes, JsonDict], + # Deprecated + bool, + ] + ], +] +SHOULD_DROP_FEDERATED_EVENT_CALLBACK = Callable[ + ["synapse.events.EventBase"], + Awaitable[Union[bool, str]], +] +USER_MAY_JOIN_ROOM_CALLBACK = Callable[ + [str, str, bool], + Awaitable[ + Union[ + Literal["NOT_SPAM"], + Codes, + # Highly experimental, not officially part of the spamchecker API, may + # disappear without warning depending on the results of ongoing + # experiments. + # Use this to return additional information as part of an error. + Tuple[Codes, JsonDict], + # Deprecated + bool, + ] + ], +] +USER_MAY_INVITE_CALLBACK = Callable[ + [str, str, str], + Awaitable[ + Union[ + Literal["NOT_SPAM"], + Codes, + # Highly experimental, not officially part of the spamchecker API, may + # disappear without warning depending on the results of ongoing + # experiments. + # Use this to return additional information as part of an error. + Tuple[Codes, JsonDict], + # Deprecated + bool, + ] + ], +] +USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[ + [str, str, str, str], + Awaitable[ + Union[ + Literal["NOT_SPAM"], + Codes, + # Highly experimental, not officially part of the spamchecker API, may + # disappear without warning depending on the results of ongoing + # experiments. + # Use this to return additional information as part of an error. + Tuple[Codes, JsonDict], + # Deprecated + bool, + ] + ], +] +USER_MAY_CREATE_ROOM_CALLBACK = Callable[ + [str], + Awaitable[ + Union[ + Literal["NOT_SPAM"], + Codes, + # Highly experimental, not officially part of the spamchecker API, may + # disappear without warning depending on the results of ongoing + # experiments. + # Use this to return additional information as part of an error. + Tuple[Codes, JsonDict], + # Deprecated + bool, + ] + ], +] +USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[ + [str, RoomAlias], + Awaitable[ + Union[ + Literal["NOT_SPAM"], + Codes, + # Highly experimental, not officially part of the spamchecker API, may + # disappear without warning depending on the results of ongoing + # experiments. + # Use this to return additional information as part of an error. + Tuple[Codes, JsonDict], + # Deprecated + bool, + ] + ], +] +USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[ + [str, str], + Awaitable[ + Union[ + Literal["NOT_SPAM"], + Codes, + # Highly experimental, not officially part of the spamchecker API, may + # disappear without warning depending on the results of ongoing + # experiments. + # Use this to return additional information as part of an error. + Tuple[Codes, JsonDict], + # Deprecated + bool, + ] + ], +] +CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[UserProfile], Awaitable[bool]] +LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[ + [ + Optional[dict], + Optional[str], + Collection[Tuple[str, str]], + ], + Awaitable[RegistrationBehaviour], +] +CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[ + [ + Optional[dict], + Optional[str], + Collection[Tuple[str, str]], + Optional[str], + ], + Awaitable[RegistrationBehaviour], +] +CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK = Callable[ + [ReadableFileWrapper, FileInfo], + Awaitable[ + Union[ + Literal["NOT_SPAM"], + Codes, + # Highly experimental, not officially part of the spamchecker API, may + # disappear without warning depending on the results of ongoing + # experiments. + # Use this to return additional information as part of an error. + Tuple[Codes, JsonDict], + # Deprecated + bool, + ] + ], +] + + +def load_legacy_spam_checkers(hs: "synapse.server.HomeServer") -> None: + """Wrapper that loads spam checkers configured using the old configuration, and + registers the spam checker hooks they implement. + """ + spam_checkers: List[Any] = [] + api = hs.get_module_api() + for module, config in hs.config.spamchecker.spam_checkers: + # Older spam checkers don't accept the `api` argument, so we + # try and detect support. + spam_args = inspect.getfullargspec(module) + if "api" in spam_args.args: + spam_checkers.append(module(config=config, api=api)) + else: + spam_checkers.append(module(config=config)) + + # The known spam checker hooks. If a spam checker module implements a method + # which name appears in this set, we'll want to register it. + spam_checker_methods = { + "check_event_for_spam", + "user_may_invite", + "user_may_create_room", + "user_may_create_room_alias", + "user_may_publish_room", + "check_username_for_spam", + "check_registration_for_spam", + "check_media_file_for_spam", + } + + for spam_checker in spam_checkers: + # Methods on legacy spam checkers might not be async, so we wrap them around a + # wrapper that will call maybe_awaitable on the result. + def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]: + # f might be None if the callback isn't implemented by the module. In this + # case we don't want to register a callback at all so we return None. + if f is None: + return None + + wrapped_func = f + + if f.__name__ == "check_registration_for_spam": + checker_args = inspect.signature(f) + if len(checker_args.parameters) == 3: + # Backwards compatibility; some modules might implement a hook that + # doesn't expect a 4th argument. In this case, wrap it in a function + # that gives it only 3 arguments and drops the auth_provider_id on + # the floor. + def wrapper( + email_threepid: Optional[dict], + username: Optional[str], + request_info: Collection[Tuple[str, str]], + auth_provider_id: Optional[str], + ) -> Union[Awaitable[RegistrationBehaviour], RegistrationBehaviour]: + # Assertion required because mypy can't prove we won't + # change `f` back to `None`. See + # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions + assert f is not None + + return f( + email_threepid, + username, + request_info, + ) + + wrapped_func = wrapper + elif len(checker_args.parameters) != 4: + raise RuntimeError( + "Bad signature for callback check_registration_for_spam", + ) + + def run(*args: Any, **kwargs: Any) -> Awaitable: + # Assertion required because mypy can't prove we won't change `f` + # back to `None`. See + # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions + assert wrapped_func is not None + + return maybe_awaitable(wrapped_func(*args, **kwargs)) + + return run + + # Register the hooks through the module API. + hooks = { + hook: async_wrapper(getattr(spam_checker, hook, None)) + for hook in spam_checker_methods + } + + api.register_spam_checker_callbacks(**hooks) + + +class SpamCheckerModuleApiCallbacks: + def __init__(self) -> None: + self.check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = [] + self.should_drop_federated_event_callbacks: List[ + SHOULD_DROP_FEDERATED_EVENT_CALLBACK + ] = [] + self.user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = [] + self.user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = [] + self.user_may_send_3pid_invite_callbacks: List[ + USER_MAY_SEND_3PID_INVITE_CALLBACK + ] = [] + self.user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = [] + self.user_may_create_room_alias_callbacks: List[ + USER_MAY_CREATE_ROOM_ALIAS_CALLBACK + ] = [] + self.user_may_publish_room_callbacks: List[USER_MAY_PUBLISH_ROOM_CALLBACK] = [] + self.check_username_for_spam_callbacks: List[ + CHECK_USERNAME_FOR_SPAM_CALLBACK + ] = [] + self.check_registration_for_spam_callbacks: List[ + CHECK_REGISTRATION_FOR_SPAM_CALLBACK + ] = [] + self.check_media_file_for_spam_callbacks: List[ + CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK + ] = [] + + def register_callbacks( + self, + check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None, + should_drop_federated_event: Optional[ + SHOULD_DROP_FEDERATED_EVENT_CALLBACK + ] = None, + user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None, + user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None, + user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None, + user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None, + user_may_create_room_alias: Optional[ + USER_MAY_CREATE_ROOM_ALIAS_CALLBACK + ] = None, + user_may_publish_room: Optional[USER_MAY_PUBLISH_ROOM_CALLBACK] = None, + check_username_for_spam: Optional[CHECK_USERNAME_FOR_SPAM_CALLBACK] = None, + check_registration_for_spam: Optional[ + CHECK_REGISTRATION_FOR_SPAM_CALLBACK + ] = None, + check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None, + ) -> None: + """Register callbacks from module for each hook.""" + if check_event_for_spam is not None: + self.check_event_for_spam_callbacks.append(check_event_for_spam) + + if should_drop_federated_event is not None: + self.should_drop_federated_event_callbacks.append( + should_drop_federated_event + ) + + if user_may_join_room is not None: + self.user_may_join_room_callbacks.append(user_may_join_room) + + if user_may_invite is not None: + self.user_may_invite_callbacks.append(user_may_invite) + + if user_may_send_3pid_invite is not None: + self.user_may_send_3pid_invite_callbacks.append( + user_may_send_3pid_invite, + ) + + if user_may_create_room is not None: + self.user_may_create_room_callbacks.append(user_may_create_room) + + if user_may_create_room_alias is not None: + self.user_may_create_room_alias_callbacks.append( + user_may_create_room_alias, + ) + + if user_may_publish_room is not None: + self.user_may_publish_room_callbacks.append(user_may_publish_room) + + if check_username_for_spam is not None: + self.check_username_for_spam_callbacks.append(check_username_for_spam) + + if check_registration_for_spam is not None: + self.check_registration_for_spam_callbacks.append( + check_registration_for_spam, + ) + + if check_media_file_for_spam is not None: + self.check_media_file_for_spam_callbacks.append(check_media_file_for_spam)