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)
diff --git a/tests/handlers/test_user_directory.py b/tests/handlers/test_user_directory.py
index a02c1c6227..eb532d4954 100644
--- a/tests/handlers/test_user_directory.py
+++ b/tests/handlers/test_user_directory.py
@@ -791,8 +791,8 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
return False
# Configure a spam checker that does not filter any users.
- spam_checker = self.hs.get_spam_checker()
- spam_checker._check_username_for_spam_callbacks = [allow_all]
+ spam_checker_callbacks = self.hs.get_module_api_callbacks().spam_checker
+ spam_checker_callbacks.check_username_for_spam_callbacks = [allow_all]
# The results do not change:
# We get one search result when searching for user2 by user1.
@@ -804,7 +804,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
# All users are spammy.
return True
- spam_checker._check_username_for_spam_callbacks = [block_all]
+ spam_checker_callbacks.check_username_for_spam_callbacks = [block_all]
# User1 now gets no search results for any of the other users.
s = self.get_success(self.handler.search_users(u1, "user2", 10))
diff --git a/tests/media/test_media_storage.py b/tests/media/test_media_storage.py
index 870047d0f2..d7ca40c6a8 100644
--- a/tests/media/test_media_storage.py
+++ b/tests/media/test_media_storage.py
@@ -31,7 +31,6 @@ from twisted.test.proto_helpers import MemoryReactor
from synapse.api.errors import Codes
from synapse.events import EventBase
-from synapse.events.spamcheck import load_legacy_spam_checkers
from synapse.http.types import QueryParams
from synapse.logging.context import make_deferred_yieldable
from synapse.media._base import FileInfo
@@ -39,6 +38,9 @@ from synapse.media.filepath import MediaFilePaths
from synapse.media.media_storage import MediaStorage, ReadableFileWrapper
from synapse.media.storage_provider import FileStorageProviderBackend
from synapse.module_api import ModuleApi
+from synapse.module_api.callbacks.spam_checker_callbacks import (
+ load_legacy_spam_checkers,
+)
from synapse.rest import admin
from synapse.rest.client import login
from synapse.server import HomeServer
diff --git a/tests/rest/client/test_rooms.py b/tests/rest/client/test_rooms.py
index a4900703c4..f946f62206 100644
--- a/tests/rest/client/test_rooms.py
+++ b/tests/rest/client/test_rooms.py
@@ -814,7 +814,8 @@ class RoomsCreateTestCase(RoomBase):
return False
join_mock = Mock(side_effect=user_may_join_room)
- self.hs.get_spam_checker()._user_may_join_room_callbacks.append(join_mock)
+ spam_checker_callbacks = self.hs.get_module_api_callbacks().spam_checker
+ spam_checker_callbacks.user_may_join_room_callbacks.append(join_mock)
channel = self.make_request(
"POST",
@@ -840,7 +841,8 @@ class RoomsCreateTestCase(RoomBase):
return Codes.CONSENT_NOT_GIVEN
join_mock = Mock(side_effect=user_may_join_room_codes)
- self.hs.get_spam_checker()._user_may_join_room_callbacks.append(join_mock)
+ spam_checker_callbacks = self.hs.get_module_api_callbacks().spam_checker
+ spam_checker_callbacks.user_may_join_room_callbacks.append(join_mock)
channel = self.make_request(
"POST",
@@ -1162,7 +1164,8 @@ class RoomJoinTestCase(RoomBase):
# `spec` argument is needed for this function mock to have `__qualname__`, which
# is needed for `Measure` metrics buried in SpamChecker.
callback_mock = Mock(side_effect=user_may_join_room, spec=lambda *x: None)
- self.hs.get_spam_checker()._user_may_join_room_callbacks.append(callback_mock)
+ spam_checker_callbacks = self.hs.get_module_api_callbacks().spam_checker
+ spam_checker_callbacks.user_may_join_room_callbacks.append(callback_mock)
# Join a first room, without being invited to it.
self.helper.join(self.room1, self.user2, tok=self.tok2)
@@ -1227,7 +1230,8 @@ class RoomJoinTestCase(RoomBase):
# `spec` argument is needed for this function mock to have `__qualname__`, which
# is needed for `Measure` metrics buried in SpamChecker.
callback_mock = Mock(side_effect=user_may_join_room, spec=lambda *x: None)
- self.hs.get_spam_checker()._user_may_join_room_callbacks.append(callback_mock)
+ spam_checker_callbacks = self.hs.get_module_api_callbacks().spam_checker
+ spam_checker_callbacks.user_may_join_room_callbacks.append(callback_mock)
# Join a first room, without being invited to it.
self.helper.join(self.room1, self.user2, tok=self.tok2)
@@ -1642,8 +1646,8 @@ class RoomMessagesTestCase(RoomBase):
return self.mock_return_value
spam_checker = SpamCheck()
-
- self.hs.get_spam_checker()._check_event_for_spam_callbacks.append(
+ spam_checker_callbacks = self.hs.get_module_api_callbacks().spam_checker
+ spam_checker_callbacks.check_event_for_spam_callbacks.append(
spam_checker.check_event_for_spam
)
@@ -3381,7 +3385,8 @@ class ThreepidInviteTestCase(unittest.HomeserverTestCase):
# `spec` argument is needed for this function mock to have `__qualname__`, which
# is needed for `Measure` metrics buried in SpamChecker.
mock = Mock(return_value=make_awaitable(True), spec=lambda *x: None)
- self.hs.get_spam_checker()._user_may_send_3pid_invite_callbacks.append(mock)
+ spam_checker_callbacks = self.hs.get_module_api_callbacks().spam_checker
+ spam_checker_callbacks.user_may_send_3pid_invite_callbacks.append(mock)
# Send a 3PID invite into the room and check that it succeeded.
email_to_invite = "teresa@example.com"
@@ -3446,7 +3451,8 @@ class ThreepidInviteTestCase(unittest.HomeserverTestCase):
return_value=make_awaitable(synapse.module_api.NOT_SPAM),
spec=lambda *x: None,
)
- self.hs.get_spam_checker()._user_may_send_3pid_invite_callbacks.append(mock)
+ spam_checker_callbacks = self.hs.get_module_api_callbacks().spam_checker
+ spam_checker_callbacks.user_may_send_3pid_invite_callbacks.append(mock)
# Send a 3PID invite into the room and check that it succeeded.
email_to_invite = "teresa@example.com"
diff --git a/tests/server.py b/tests/server.py
index 5de9722766..4699ef2e1e 100644
--- a/tests/server.py
+++ b/tests/server.py
@@ -72,11 +72,13 @@ from twisted.web.server import Request, Site
from synapse.config.database import DatabaseConnectionConfig
from synapse.config.homeserver import HomeServerConfig
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 SynapseRequest
from synapse.logging.context import ContextResourceUsage
+from synapse.module_api.callbacks.spam_checker_callbacks import (
+ load_legacy_spam_checkers,
+)
from synapse.server import HomeServer
from synapse.storage import DataStore
from synapse.storage.engines import PostgresEngine, create_engine
|