mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-05-03 04:44:52 -04:00
Standardise the module interface (#10062)
This PR adds a common configuration section for all modules (see docs). These modules are then loaded at startup by the homeserver. Modules register their hooks and web resources using the new `register_[...]_callbacks` and `register_web_resource` methods of the module API.
This commit is contained in:
parent
91fa9cca99
commit
1b3e398bea
23 changed files with 768 additions and 187 deletions
|
@ -15,7 +15,18 @@
|
|||
|
||||
import inspect
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Collection, Dict, List, Optional, Tuple, Union
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Awaitable,
|
||||
Callable,
|
||||
Collection,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
from synapse.rest.media.v1._base import FileInfo
|
||||
from synapse.rest.media.v1.media_storage import ReadableFileWrapper
|
||||
|
@ -29,20 +40,186 @@ if TYPE_CHECKING:
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CHECK_EVENT_FOR_SPAM_CALLBACK = Callable[
|
||||
["synapse.events.EventBase"],
|
||||
Awaitable[Union[bool, str]],
|
||||
]
|
||||
USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]]
|
||||
USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]]
|
||||
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[[str, RoomAlias], Awaitable[bool]]
|
||||
USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[[str, str], Awaitable[bool]]
|
||||
CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[Dict[str, str]], 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[bool],
|
||||
]
|
||||
|
||||
|
||||
def load_legacy_spam_checkers(hs: "synapse.server.HomeServer"):
|
||||
"""Wrapper that loads spam checkers configured using the old configuration, and
|
||||
registers the spam checker hooks they implement.
|
||||
"""
|
||||
spam_checkers = [] # type: List[Any]
|
||||
api = hs.get_module_api()
|
||||
for module, config in hs.config.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
|
||||
|
||||
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]:
|
||||
# We've already made sure f is not None above, but mypy doesn't
|
||||
# do well across function boundaries so we need to tell it f is
|
||||
# definitely not None.
|
||||
assert f is not None
|
||||
|
||||
return f(
|
||||
email_threepid,
|
||||
username,
|
||||
request_info,
|
||||
)
|
||||
|
||||
f = wrapper
|
||||
elif len(checker_args.parameters) != 4:
|
||||
raise RuntimeError(
|
||||
"Bad signature for callback check_registration_for_spam",
|
||||
)
|
||||
|
||||
def run(*args, **kwargs):
|
||||
# We've already made sure f is not None above, but mypy doesn't do well
|
||||
# across function boundaries so we need to tell it f is definitely not
|
||||
# None.
|
||||
assert f is not None
|
||||
|
||||
return maybe_awaitable(f(*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:
|
||||
def __init__(self, hs: "synapse.server.HomeServer"):
|
||||
self.spam_checkers = [] # type: List[Any]
|
||||
api = hs.get_module_api()
|
||||
def __init__(self):
|
||||
self._check_event_for_spam_callbacks: List[CHECK_EVENT_FOR_SPAM_CALLBACK] = []
|
||||
self._user_may_invite_callbacks: List[USER_MAY_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
|
||||
] = []
|
||||
|
||||
for module, config in hs.config.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:
|
||||
self.spam_checkers.append(module(config=config, api=api))
|
||||
else:
|
||||
self.spam_checkers.append(module(config=config))
|
||||
def register_callbacks(
|
||||
self,
|
||||
check_event_for_spam: Optional[CHECK_EVENT_FOR_SPAM_CALLBACK] = None,
|
||||
user_may_invite: Optional[USER_MAY_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,
|
||||
):
|
||||
"""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 user_may_invite is not None:
|
||||
self._user_may_invite_callbacks.append(user_may_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)
|
||||
|
||||
async def check_event_for_spam(
|
||||
self, event: "synapse.events.EventBase"
|
||||
|
@ -60,9 +237,10 @@ class SpamChecker:
|
|||
True or a string if the event is spammy. If a string is returned it
|
||||
will be used as the error message returned to the user.
|
||||
"""
|
||||
for spam_checker in self.spam_checkers:
|
||||
if await maybe_awaitable(spam_checker.check_event_for_spam(event)):
|
||||
return True
|
||||
for callback in self._check_event_for_spam_callbacks:
|
||||
res = await callback(event) # type: Union[bool, str]
|
||||
if res:
|
||||
return res
|
||||
|
||||
return False
|
||||
|
||||
|
@ -81,15 +259,8 @@ class SpamChecker:
|
|||
Returns:
|
||||
True if the user may send an invite, otherwise False
|
||||
"""
|
||||
for spam_checker in self.spam_checkers:
|
||||
if (
|
||||
await maybe_awaitable(
|
||||
spam_checker.user_may_invite(
|
||||
inviter_userid, invitee_userid, room_id
|
||||
)
|
||||
)
|
||||
is False
|
||||
):
|
||||
for callback in self._user_may_invite_callbacks:
|
||||
if await callback(inviter_userid, invitee_userid, room_id) is False:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -105,11 +276,8 @@ class SpamChecker:
|
|||
Returns:
|
||||
True if the user may create a room, otherwise False
|
||||
"""
|
||||
for spam_checker in self.spam_checkers:
|
||||
if (
|
||||
await maybe_awaitable(spam_checker.user_may_create_room(userid))
|
||||
is False
|
||||
):
|
||||
for callback in self._user_may_create_room_callbacks:
|
||||
if await callback(userid) is False:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -128,13 +296,8 @@ class SpamChecker:
|
|||
Returns:
|
||||
True if the user may create a room alias, otherwise False
|
||||
"""
|
||||
for spam_checker in self.spam_checkers:
|
||||
if (
|
||||
await maybe_awaitable(
|
||||
spam_checker.user_may_create_room_alias(userid, room_alias)
|
||||
)
|
||||
is False
|
||||
):
|
||||
for callback in self._user_may_create_room_alias_callbacks:
|
||||
if await callback(userid, room_alias) is False:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -151,13 +314,8 @@ class SpamChecker:
|
|||
Returns:
|
||||
True if the user may publish the room, otherwise False
|
||||
"""
|
||||
for spam_checker in self.spam_checkers:
|
||||
if (
|
||||
await maybe_awaitable(
|
||||
spam_checker.user_may_publish_room(userid, room_id)
|
||||
)
|
||||
is False
|
||||
):
|
||||
for callback in self._user_may_publish_room_callbacks:
|
||||
if await callback(userid, room_id) is False:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
@ -177,15 +335,11 @@ class SpamChecker:
|
|||
Returns:
|
||||
True if the user is spammy.
|
||||
"""
|
||||
for spam_checker in self.spam_checkers:
|
||||
# For backwards compatibility, only run if the method exists on the
|
||||
# spam checker
|
||||
checker = getattr(spam_checker, "check_username_for_spam", None)
|
||||
if checker:
|
||||
# Make a copy of the user profile object to ensure the spam checker
|
||||
# cannot modify it.
|
||||
if await maybe_awaitable(checker(user_profile.copy())):
|
||||
return True
|
||||
for callback in self._check_username_for_spam_callbacks:
|
||||
# Make a copy of the user profile object to ensure the spam checker cannot
|
||||
# modify it.
|
||||
if await callback(user_profile.copy()):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
@ -211,33 +365,13 @@ class SpamChecker:
|
|||
Enum for how the request should be handled
|
||||
"""
|
||||
|
||||
for spam_checker in self.spam_checkers:
|
||||
# For backwards compatibility, only run if the method exists on the
|
||||
# spam checker
|
||||
checker = getattr(spam_checker, "check_registration_for_spam", None)
|
||||
if checker:
|
||||
# Provide auth_provider_id if the function supports it
|
||||
checker_args = inspect.signature(checker)
|
||||
if len(checker_args.parameters) == 4:
|
||||
d = checker(
|
||||
email_threepid,
|
||||
username,
|
||||
request_info,
|
||||
auth_provider_id,
|
||||
)
|
||||
elif len(checker_args.parameters) == 3:
|
||||
d = checker(email_threepid, username, request_info)
|
||||
else:
|
||||
logger.error(
|
||||
"Invalid signature for %s.check_registration_for_spam. Denying registration",
|
||||
spam_checker.__module__,
|
||||
)
|
||||
return RegistrationBehaviour.DENY
|
||||
|
||||
behaviour = await maybe_awaitable(d)
|
||||
assert isinstance(behaviour, RegistrationBehaviour)
|
||||
if behaviour != RegistrationBehaviour.ALLOW:
|
||||
return behaviour
|
||||
for callback in self._check_registration_for_spam_callbacks:
|
||||
behaviour = await (
|
||||
callback(email_threepid, username, request_info, auth_provider_id)
|
||||
)
|
||||
assert isinstance(behaviour, RegistrationBehaviour)
|
||||
if behaviour != RegistrationBehaviour.ALLOW:
|
||||
return behaviour
|
||||
|
||||
return RegistrationBehaviour.ALLOW
|
||||
|
||||
|
@ -275,13 +409,9 @@ class SpamChecker:
|
|||
allowed.
|
||||
"""
|
||||
|
||||
for spam_checker in self.spam_checkers:
|
||||
# For backwards compatibility, only run if the method exists on the
|
||||
# spam checker
|
||||
checker = getattr(spam_checker, "check_media_file_for_spam", None)
|
||||
if checker:
|
||||
spam = await maybe_awaitable(checker(file_wrapper, file_info))
|
||||
if spam:
|
||||
return True
|
||||
for callback in self._check_media_file_for_spam_callbacks:
|
||||
spam = await callback(file_wrapper, file_info)
|
||||
if spam:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue